Skip to content

Durable Entities

Note: This recipe covers Durable Entities (the stateful entity model) with Azure Functions Python v2 using the blueprint model. Entity functions require Durable Functions 2.0 or later.

Overview

Durable Entities (entity functions) manage small pieces of explicit state — think of them as tiny, addressable, single-threaded objects that live in durable storage. Unlike orchestrator functions, which represent state implicitly through control flow, an entity function reads and writes its state explicitly through operations.

Entities are ideal for:

  • Aggregation — accumulate values from many sources (counters, running totals, event tallies).
  • Fan-in of high-volume signals — thousands of producers updating a small shared state without lock contention on a database row.
  • Actor-style state — one addressable object per user, device, cart, or game session.
Concept Description
Entity function Defines the operations that read and update a piece of state. Uses the entity_trigger.
Entity ID A pair of strings — the entity name (the entity type, e.g. Counter) plus the entity key (the unique instance, e.g. a user ID). Written as @Counter@user-42.
Operation A named action the entity supports (for example add, reset, get), with optional input.
Serialized access A single entity processes its operations one at a time, so you never need locks to protect the state.
flowchart TD
    CLIENT["Client Function\nsignal_entity() / read_entity_state()"] -->|"signal (one-way)"| ENT["Entity Function\nCounter@key\noperations run serially"]
    ORC["Orchestrator Function\ncall_entity() / signal_entity()"] -->|"call (two-way)"| ENT
    ENT -->|"set_state()"| STORE["Durable Store\npersisted entity state"]
    STORE -->|"read_entity_state()"| CLIENT
    style ENT fill:#0078d4,color:#fff
    style STORE fill:#107c10,color:#fff

Prerequisites

Add the Durable Functions package to requirements.txt:

azure-functions-durable>=1.2.0

Ensure your host.json has the extension bundle:

{
  "version": "2.0",
  "extensionBundle": {
    "id": "Microsoft.Azure.Functions.ExtensionBundle",
    "version": "[4.*, 5.0.0)"
  }
}

Like orchestrations, entities persist their state in Azure Storage (AzureWebJobsStorage). On Flex Consumption, configure host storage with identity-based settings (for example AzureWebJobsStorage__accountName) rather than a connection string.

Define an Entity Function

The entity function receives a DurableEntityContext. Read the current state (with a default factory), branch on the operation name, mutate the value, and persist it with set_state. Use set_result to return a value to a two-way caller.

import azure.functions as func
import azure.durable_functions as df
import json

bp = df.Blueprint()


@bp.entity_trigger(context_name="context")
def Counter(context: df.DurableEntityContext):
    """A durable counter entity supporting add / reset / get operations."""
    current_value = context.get_state(lambda: 0)
    operation = context.operation_name

    if operation == "add":
        current_value += context.get_input()
    elif operation == "reset":
        current_value = 0
    elif operation == "get":
        context.set_result(current_value)

    context.set_state(current_value)

Key points:

  • context.get_state(lambda: 0) returns the persisted state, or 0 the first time the entity is used.
  • context.operation_name is the operation string chosen by the caller.
  • context.get_input() reads the operation's optional input.
  • context.set_result(value) returns a value to callers that used two-way call_entity (ignored by one-way signals).
  • context.set_state(value) persists the new state. The runtime always writes state to storage after the operation completes.

Signal an Entity from a Client Function

Signaling is one-way (fire-and-forget): the client sends an operation and does not wait for a result. Client functions can signal entities and read their state, but cannot call them for a return value.

@bp.route(route="entities/counter/{key}/add", methods=["POST"])
@bp.durable_client_input(client_name="client")
async def add_to_counter(req: func.HttpRequest, client: df.DurableOrchestrationClient) -> func.HttpResponse:
    """Fire-and-forget: increment a named counter."""
    key = req.route_params.get("key")
    amount = int(req.params.get("amount", "1"))

    entity_id = df.EntityId("Counter", key)
    await client.signal_entity(entity_id, "add", amount)

    return func.HttpResponse(
        json.dumps({"entity": f"@Counter@{key}", "signaled": "add", "amount": amount}),
        mimetype="application/json",
        status_code=202,
    )

Read Entity State from a Client Function

Reading returns the entity's most recently persisted (committed) state. It may be slightly stale relative to the entity's in-memory state, but it never reflects a half-completed operation.

@bp.route(route="entities/counter/{key}", methods=["GET"])
@bp.durable_client_input(client_name="client")
async def get_counter(req: func.HttpRequest, client: df.DurableOrchestrationClient) -> func.HttpResponse:
    """Read the committed state of a named counter."""
    key = req.route_params.get("key")
    entity_id = df.EntityId("Counter", key)

    state = await client.read_entity_state(entity_id)

    return func.HttpResponse(
        json.dumps({
            "entity": f"@Counter@{key}",
            "exists": state.entity_exists,
            "value": state.entity_state if state.entity_exists else 0,
        }),
        mimetype="application/json",
        status_code=200,
    )

Call and Signal an Entity from an Orchestrator

Orchestrators can both call an entity (two-way — wait for a result) and signal it (one-way). This lets you read shared state, make a decision, and then update it atomically per entity without database locking.

sequenceDiagram
    participant C as Client
    participant O as Orchestrator
    participant E as Entity Counter@game1
    C->>E: signal_entity("add", 1) [one-way]
    Note over E: operation queued, no response
    O->>E: call_entity("get") [two-way]
    E-->>O: current value
    Note over E: operations run one at a time (serialized)
    O->>E: signal_entity("add", 1) [one-way]
    C->>E: read_entity_state()
    E-->>C: last committed state
@bp.orchestration_trigger(context_name="context")
def reserve_seat(context: df.DurableOrchestrationContext):
    """Reserve a seat if the game is not sold out (capacity = 100)."""
    game_id = context.get_input()
    entity_id = df.EntityId("Counter", game_id)

    # Two-way call: read the current seat count and wait for the result.
    current = yield context.call_entity(entity_id, "get")

    if current >= 100:
        return {"reserved": False, "reason": "sold out", "seats_taken": current}

    # One-way signal: claim a seat (no need to wait for a response).
    context.signal_entity(entity_id, "add", 1)
    return {"reserved": True, "seat_number": current + 1}

Do not use entities for cross-entity transactions

Each entity is consistent on its own, but an operation that must update two entities atomically is not transactional across them. Model such invariants inside a single entity, or coordinate them from an orchestrator that tolerates partial progress and compensates on failure.

Access Rules Summary

Caller Signal (one-way) Call (two-way) Read state
Client function Yes No Yes
Orchestrator function Yes Yes No (call get instead)
Entity function Yes No

Try It Locally

# Increment two independent counters (each is its own entity instance)
curl --request POST "http://localhost:7071/api/entities/counter/game1/add?amount=3"
curl --request POST "http://localhost:7071/api/entities/counter/game2/add?amount=1"

# Read committed state for game1
curl "http://localhost:7071/api/entities/counter/game1"

See Also

Sources