# How to Handle Async Contract Operations in GenLayer: A Complete Guide

> Learn to handle async contract operations in GenLayer using a state-machine pattern with gl nondet functions and validators. Master asynchronous workflows in your GenLayer projects.

- Repository: [GenLayer Labs/genlayer-project-boilerplate](https://github.com/genlayerlabs/genlayer-project-boilerplate)
- Tags: how-to-guide
- Published: 2026-08-20

---

**GenLayer contracts cannot use Python's native `async`/`await` primitives; instead, use a two-step state-machine pattern with `gl.nondet.*` functions and equivalence-principle validators to handle asynchronous-like workflows.**

The GenLayer platform enables intelligent smart contracts that interact with external data sources, but its deterministic execution model requires a different approach to handling operations that would traditionally be asynchronous. In `genlayerlabs/genlayer-project-boilerplate`, the reference implementation demonstrates how to structure contracts that fetch remote data or invoke LLM prompts across multiple transactions.

## Why GenLayer Prohibits Native Async/Await

GenLayer smart contracts execute inside the **GenVM**, a deterministic virtual machine that guarantees consensus across all validators. This design has a critical constraint: **contracts must run to completion in a single transaction** without yielding control or suspending execution.

Native Python `async`/`await` introduces nondeterminism through coroutine scheduling, which would break validator consensus. The commented line in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) illustrates this limitation:

```python

# Line 65 – This would be ideal but is NOT allowed in GenLayer:

# match_status = await self._check_match(bet.resolution_url, bet.team1, bet.team2)

```

Instead, all nondeterministic operations are performed **synchronously** through the GenLayer SDK, with results validated via equivalence principles.

## The Two-Step Pattern for Async-Like Operations

To model workflows that span multiple transactions, split your logic into distinct phases:

1. **Initiation** – Store request parameters in contract state; emit events but do not call `gl.nondet.*` functions.
2. **Resolution** – In a separate transaction, execute the nondeterministic call and apply the validated result.

This pattern is fully implemented in the `FootballBets` contract at [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py).

### Step 1: Initiate Without Nondeterministic Calls

The `create_bet` method (lines 70–87) demonstrates proper initiation:

```python
@gl.public.write
def create_bet(
    self,
    game_date: str,
    team1: str,
    team2: str,
    predicted_winner: str
) -> str:
    # Generate deterministic data for later resolution

    match_resolution_url = (
        f"https://www.bbc.com/sport/football/scores-fixtures/{game_date}"
    )
    
    bet_id = f"{gl.message.sender_address}_{team1}_{team2}_{game_date}"
    
    # Store state WITHOUT making any nondet calls

    self.bets[gl.message.sender_address][bet_id] = Bet(
        game_date=game_date,
        team1=team1,
        team2=team2,
        predicted_winner=predicted_winner,
        resolution_url=match_resolution_url,
        has_resolved=False
    )
    
    return bet_id

```

Key characteristics of the initiation phase:
- All operations are **deterministic and pure**
- External URLs or prompts are **stored, not executed**
- Contract state tracks the pending request

### Step 2: Resolve With Synchronous Nondeterministic Calls

The `resolve_bet` method (lines 90–103) performs the actual fetch:

```python
@gl.public.write
def resolve_bet(self, bet_id: str):
    # Guard clauses ensure proper state

    if bet_id not in self.bets[gl.message.sender_address]:
        raise Exception("Invalid bet ID")
    
    bet = self.bets[gl.message.sender_address][bet_id]
    
    if bet.has_resolved:
        raise Exception("Bet already resolved")
    
    # SYNCHRONOUS nondeterministic call (no await)

    match_status = self._check_match(
        bet.resolution_url,
        bet.team1,
        bet.team2
    )
    
    # Apply result deterministically

    bet.has_resolved = True
    # ... determine winner, update balances ...

```

The `_check_match` helper (lines 30–55) encapsulates the LLM prompt and validation:

```python
def _check_match(self, url: str, team1: str, team2: str) -> dict:
    # Fetch remote HTML synchronously

    page_contents = gl.nondet.web.render(url, mode="text")
    
    # Build prompt for LLM extraction

    prompt = f"""
    Based on the following content from {url},
    what was the result of the match between {team1} and {team2}?
    Return a JSON with keys: home_score, away_score.
    Content: {page_contents}
    """
    
    # Execute LLM prompt synchronously

    raw_response = gl.nondet.exec_prompt(prompt)
    
    # Equivalence-principle validator ensures consensus

    def parse_response() -> dict:
        return json.loads(raw_response)
    
    # All validators must agree on this exact output

    validated_result = gl.eq_principle.strict_eq(parse_response)
    return validated_result

```

## Core GenLayer SDK Components

| Component | Purpose | Example Usage |
|-----------|---------|---------------|
| **`gl.nondet.web.render`** | Fetch remote web content | `gl.nondet.web.render(url, mode="text"` |
| **`gl.nondet.exec_prompt`** | Execute LLM prompts | `gl.nondet.exec_prompt(prompt_string)` |
| **`gl.eq_principle.strict_eq`** | Exact-match validator for simple outputs | `gl.eq_principle.strict_eq(lambda: result)` |
| **`gl.eq_principle.prompt_comparative`** | LLM-based semantic comparison validator | For fuzzy or structured data matching |
| **`gl.public.write`** | Decorator for state-modifying functions | Marks transaction entry points |

## Complete Example: Generic Async Pattern

Apply this template to your own contracts:

```python
import json

class AsyncOperationContract:
    def __init__(self):
        # Track pending operations by caller

        self.pending_requests: dict[str, str] = {}
        self.results: dict[str, str] = {}
    
    @gl.public.write
    def start_operation(self, resource_url: str) -> str:
        """
        Phase 1: Store the request parameters.
        No nondeterministic calls allowed here.
        """
        request_id = f"{gl.message.sender_address}_{gl.block.number}"
        self.pending_requests[gl.message.sender_address] = resource_url
        return request_id
    
    @gl.public.write
    def complete_operation(self) -> str:
        """
        Phase 2: Execute fetch and validate result.
        This runs in a separate transaction initiated later.
        """
        url = self.pending_requests.get(gl.message.sender_address)
        if not url:
            raise Exception("No pending request for caller")
        
        # Nondeterministic fetch (synchronous)

        raw_content = gl.nondet.web.render(url, mode="text")
        
        # Define validator function

        def extract_data() -> str:
            # Deterministic processing of fetched content

            return raw_content[:1000]  # Truncate for example

        
        # Consensus validation

        validated = gl.eq_principle.strict_eq(extract_data)
        
        # Cleanup and store

        del self.pending_requests[gl.message.sender_address]
        self.results[gl.message.sender_address] = validated
        
        return validated

```

## Error Handling Best Practices

The `resolve_bet` implementation demonstrates defensive patterns:

- **State validation before nondet calls**: Check `has_resolved`, existence of records, and authorization before consuming validator resources.
- **Deterministic exceptions**: Use standard `raise Exception(...)` for control flow—never catch nondeterministic errors.
- **Idempotent resolution design**: Ensure multiple valid calls produce consistent final state.

From [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) lines 91–99:

```python
if bet_id not in self.bets[gl.message.sender_address]:
    raise Exception("Invalid bet ID")

bet = self.bets[gl.message.sender_address][bet_id]

if bet.has_resolved:
    raise Exception("Bet already resolved")  # Prevent double-resolution

```

## Testing Async Contract Patterns

The repository includes [`tests/direct/test_resolve_bet.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/tests/direct/test_resolve_bet.py) for validating the two-step flow:

```python

# Typical test sequence

def test_full_bet_lifecycle(contract_client):
    # Phase 1: Create (deterministic)

    bet_id = contract_client.create_bet(
        game_date="2024-01-15",
        team1="Arsenal",
        team2="Chelsea",
        predicted_winner="Arsenal"
    )
    
    # Phase 2: Resolve (nondeterministic, validated)

    result = contract_client.resolve_bet(bet_id)
    assert result["has_resolved"] is True

```

Run tests with the commands documented in [`CLAUDE.md`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/CLAUDE.md):

```bash

# Lint contract code

python -m py_contracts lint contracts/

# Run direct tests

pytest tests/direct/test_resolve_bet.py -v

```

## Summary

- **No `async`/`await` in GenLayer**: The GenVM requires single-transaction determinism.
- **Split into initiate + resolve**: Store parameters first, execute `gl.nondet.*` calls later.
- **Always validate with equivalence principles**: Use `gl.eq_principle.strict_eq` or `gl.eq_principle.prompt_comparative` to ensure validator consensus.
- **Reference implementation**: Study [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py) for production patterns.

## Frequently Asked Questions

### Can I use Python's asyncio library in GenLayer contracts?

No. The GenVM executes contracts deterministically across all validators, and `asyncio`'s event loop introduces scheduling nondeterminism. All I/O operations must use synchronous `gl.nondet.*` primitives wrapped in equivalence-principle validators, as implemented in [`contracts/football_bets.py`](https://github.com/genlayerlabs/genlayer-project-boilerplate/blob/main/contracts/football_bets.py).

### How do I handle long-running external API calls?

Store the API parameters in contract state during an initial transaction. Trigger the actual call in a subsequent transaction when needed. The external latency is absorbed between transactions, not within one. The `FootballBets` contract defers match result fetching from `create_bet` to `resolve_bet` for this reason.

### What happens if validators disagree on a nondeterministic result?

The equivalence-principle validator rejects the transaction. Contracts must handle this by either retrying with adjusted parameters or leaving the request in pending state for future resolution. `gl.eq_principle.strict_eq` requires byte-identical outputs; use `gl.eq_principle.prompt_comparative` for semantically equivalent but textually different responses.

### Can multiple pending requests exist per user?

Yes. The storage pattern maps user addresses to nested structures (e.g., `self.bets[address][bet_id]`). Design your initiation function to generate unique IDs—typically combining sender address, block number, and content hash—to enable concurrent pending operations.