Durable Workflow Example¶
This walkthrough creates a Durable Functions project with an orchestrator, activities, and an HTTP starter endpoint, then runs everything locally.
What You Will Build¶
You will create a durable workflow that:
- starts an orchestration via an HTTP POST
- calls multiple activity functions sequentially
- collects results and returns them through a status endpoint
- runs locally with Azurite and Core Tools
1) Generate the Project¶
Set up environment and dependencies:
Run quality checks:
2) Understand the Durable Pattern¶
The generated project contains three function types in a single module:
HTTP Starter — receives a POST request and starts a new orchestration instance:
@durable_blueprint.route(
route="orchestrators/{functionName}",
methods=["POST"],
auth_level=func.AuthLevel.ANONYMOUS,
)
@durable_blueprint.durable_client_input(client_name="client")
async def http_start(
req: func.HttpRequest, client: df.DurableOrchestrationClient
) -> func.HttpResponse:
instance_id = await client.start_new(req.route_params["functionName"])
logging.info("Started orchestration with ID '%s'.", instance_id)
return client.create_check_status_response(req, instance_id)
Orchestrator — coordinates activity calls using generator-style yield:
@durable_blueprint.orchestration_trigger(context_name="context")
def hello_orchestrator(context: df.DurableOrchestrationContext) -> list[str]:
results: list[str] = []
results.append(yield context.call_activity("say_hello", "Tokyo"))
results.append(yield context.call_activity("say_hello", "Seattle"))
results.append(yield context.call_activity("say_hello", "London"))
return results
Activity — performs a single unit of work:
@durable_blueprint.activity_trigger(input_name="city")
def say_hello(city: str) -> str:
return build_greeting(city)
Durable Functions imports
Durable uses azure.functions.durable_functions (imported as df)
alongside the standard azure.functions package.
3) Generated Layout¶
my-workflow/
├── function_app.py
├── app/
│ ├── functions/durable.py
│ └── services/durable_service.py
├── tests/test_durable.py
├── local.settings.json.example
└── pyproject.toml
app/functions/durable.py— all three function types (starter, orchestrator, activity).app/services/durable_service.py— business logic called by activities.tests/test_durable.py— tests service functions directly.
4) Azurite Setup¶
Durable Functions require AzureWebJobsStorage for orchestration state
management. Start Azurite before running locally.
Using npm:
Using Docker:
docker run --rm -p 10000:10000 -p 10001:10001 -p 10002:10002 \
mcr.microsoft.com/azure-storage/azurite
Create active local settings file:
5) Run and Test Locally¶
Start the orchestration by sending a POST to the starter endpoint:
The response includes a statusQueryGetUri field — use that URL directly
to poll orchestration progress:
Expected completed output:
6) Customize the Workflow¶
Add a new activity and wire it into the orchestrator.
Add a formatting function to app/services/durable_service.py:
Add the activity in app/functions/durable.py:
@durable_blueprint.activity_trigger(input_name="greeting")
def format_result(greeting: str) -> str:
return format_greeting(greeting)
Update the orchestrator to call the new activity after each greeting:
results.append(yield context.call_activity("say_hello", "Tokyo"))
results.append(yield context.call_activity("format_result", results[-1]))
7) Testing Strategy¶
The generated test validates the service layer directly:
def test_build_greeting_returns_city_greeting() -> None:
result = build_greeting("Tokyo")
assert result == "Hello, Tokyo!"
Run tests:
Durable orchestrations are best tested through their service functions rather
than the orchestration framework. Keep activity logic in app/services/ and
write unit tests against those functions.
Keep activities thin
Treat activity functions as adapters: extract inputs, call a service function, return the result. Test the service function.
Troubleshooting Notes¶
Orchestration not starting
Verify Azurite is running and local.settings.json has
AzureWebJobsStorage configured.
Status URL returns 404
The {functionName} in the route must match the decorated orchestrator
function name exactly (hello_orchestrator).
Orchestrator yield syntax
Orchestrators use Python generators with yield, not async/await.
Each yield context.call_activity(...) suspends until the activity
completes.
Next Steps¶
- See Worker Triggers for queue, blob, and messaging patterns.
- See Templates for the full template list.
- See Troubleshooting for runtime diagnostics.