|
| 1 | +# Migrate from Azure Functions Durable 1.x to 2.x |
| 2 | + |
| 3 | +This guide explains how to move an application from |
| 4 | +`azure-functions-durable` 1.x to 2.x. Version 2.x is a ground-up rewrite built |
| 5 | +on the [`durabletask`](https://pypi.org/project/durabletask/) SDK and its gRPC |
| 6 | +runtime. |
| 7 | + |
| 8 | +> [!WARNING] |
| 9 | +> `azure-functions-durable` 2.x is currently in preview. Its APIs may change |
| 10 | +> before the stable 2.0.0 release. |
| 11 | +
|
| 12 | +## Choose a migration path |
| 13 | + |
| 14 | +Version 2.x includes compatibility shims for most 1.x APIs used by |
| 15 | +decorator-based apps. This supports a staged migration: |
| 16 | + |
| 17 | +1. **Compatibility upgrade:** update the runtime and dependencies while keeping |
| 18 | + existing one-argument orchestrators, entities, and client calls. These APIs |
| 19 | + continue to work but emit `DeprecationWarning` where a native replacement |
| 20 | + exists. |
| 21 | +2. **Native API migration:** move to durabletask-native function signatures, |
| 22 | + client methods, retry policies, and entity types. |
| 23 | + |
| 24 | +Use the compatibility upgrade first when minimizing application changes is more |
| 25 | +important than adopting the new API immediately. |
| 26 | + |
| 27 | +## Requirements and breaking changes |
| 28 | + |
| 29 | +Before upgrading, account for these 2.x requirements: |
| 30 | + |
| 31 | +- **Python 3.13 or later is required.** |
| 32 | +- **Only the Azure Functions Python V2 programming model is supported.** Apps |
| 33 | + must use `DFApp` or `Blueprint` decorators. The classic programming model with |
| 34 | + one directory and `function.json` per function is not supported. |
| 35 | +- **A modern Durable Functions host is required.** Local sample apps use the |
| 36 | + preview extension bundle shown below. |
| 37 | +- **The OpenAI Agents integration was removed.** The |
| 38 | + `azure.durable_functions.openai_agents` package is not available in 2.x. |
| 39 | +- **The primary APIs now come from `durabletask`.** Compatibility aliases are |
| 40 | + available to stage the migration, but new code should use the native names. |
| 41 | + |
| 42 | +If the app still uses the classic Python programming model, migrate it to the |
| 43 | +[decorator-based model](https://learn.microsoft.com/azure/azure-functions/functions-reference-python) |
| 44 | +before upgrading this package. |
| 45 | + |
| 46 | +## Prepare running orchestrations |
| 47 | + |
| 48 | +Orchestrator code must remain deterministic and replay-compatible for existing |
| 49 | +instances. Before changing function names, activity schedules, branching logic, |
| 50 | +or serialized inputs: |
| 51 | + |
| 52 | +- allow existing instances to finish; or |
| 53 | +- deploy the migrated app with a new task hub and route new instances there. |
| 54 | + |
| 55 | +Test replay behavior with representative orchestration histories in a |
| 56 | +non-production environment before updating a task hub that contains running |
| 57 | +instances. |
| 58 | + |
| 59 | +## Perform a compatibility upgrade |
| 60 | + |
| 61 | +### 1. Update the Python runtime |
| 62 | + |
| 63 | +Update local development, CI, and the deployed Function App to Python 3.13 or |
| 64 | +later. |
| 65 | + |
| 66 | +### 2. Update dependencies |
| 67 | + |
| 68 | +Replace the 1.x package constraint in `requirements.txt`: |
| 69 | + |
| 70 | +```text |
| 71 | +azure-functions>=2.3.0b2 |
| 72 | +azure-functions-durable>=2.0.0b1,<3 |
| 73 | +``` |
| 74 | + |
| 75 | +### 3. Update the extension bundle |
| 76 | + |
| 77 | +Use the preview extension bundle that contains the required Durable Task gRPC |
| 78 | +runtime: |
| 79 | + |
| 80 | +```json |
| 81 | +{ |
| 82 | + "version": "2.0", |
| 83 | + "extensionBundle": { |
| 84 | + "id": "Microsoft.Azure.Functions.ExtensionBundle.Preview", |
| 85 | + "version": "[4.*, 5.0.0)" |
| 86 | + }, |
| 87 | + "extensions": { |
| 88 | + "durableTask": { |
| 89 | + "storageProvider": { |
| 90 | + "type": "AzureStorage" |
| 91 | + } |
| 92 | + } |
| 93 | + } |
| 94 | +} |
| 95 | +``` |
| 96 | + |
| 97 | +### 4. Keep supported 1.x code temporarily |
| 98 | + |
| 99 | +A decorator-based 1.x orchestrator and client starter can run through the 2.x |
| 100 | +compatibility layer without changing their function shape: |
| 101 | + |
| 102 | +```python |
| 103 | +import azure.functions as func |
| 104 | +import azure.durable_functions as df |
| 105 | + |
| 106 | +app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) |
| 107 | + |
| 108 | + |
| 109 | +@app.orchestration_trigger(context_name="context") |
| 110 | +def hello(context: df.DurableOrchestrationContext): |
| 111 | + name = context.get_input() |
| 112 | + return (yield context.call_activity("say_hello", name)) |
| 113 | + |
| 114 | + |
| 115 | +@app.route(route="start", methods=["POST"]) |
| 116 | +@app.durable_client_input(client_name="client") |
| 117 | +async def start( |
| 118 | + req: func.HttpRequest, |
| 119 | + client: df.DurableFunctionsClient) -> func.HttpResponse: |
| 120 | + instance_id = await client.start_new("hello", client_input="Tokyo") |
| 121 | + return client.create_check_status_response(req, instance_id) |
| 122 | +``` |
| 123 | + |
| 124 | +The single-argument orchestrator receives the compatibility |
| 125 | +`DurableOrchestrationContext`, and `start_new` delegates to the native client. |
| 126 | +Use this state to validate the infrastructure upgrade before changing |
| 127 | +application APIs. |
| 128 | + |
| 129 | +## Migrate to native orchestration APIs |
| 130 | + |
| 131 | +Native orchestrators accept the durabletask context and input as separate |
| 132 | +arguments: |
| 133 | + |
| 134 | +```python |
| 135 | +from typing import Any |
| 136 | + |
| 137 | +import azure.functions as func |
| 138 | +import azure.durable_functions as df |
| 139 | +from durabletask import task |
| 140 | + |
| 141 | +app = df.DFApp(http_auth_level=func.AuthLevel.ANONYMOUS) |
| 142 | + |
| 143 | + |
| 144 | +@app.orchestration_trigger(context_name="context") |
| 145 | +def hello(ctx: task.OrchestrationContext, name: Any): |
| 146 | + return (yield ctx.call_activity("say_hello", input=name)) |
| 147 | + |
| 148 | + |
| 149 | +@app.route(route="start", methods=["POST"]) |
| 150 | +@app.durable_client_input(client_name="client") |
| 151 | +async def start( |
| 152 | + req: func.HttpRequest, |
| 153 | + client: df.DurableFunctionsClient) -> func.HttpResponse: |
| 154 | + instance_id = await client.schedule_new_orchestration( |
| 155 | + "hello", input="Tokyo") |
| 156 | + return client.create_check_status_response(req, instance_id) |
| 157 | +``` |
| 158 | + |
| 159 | +Activity functions can keep their existing single-input signature: |
| 160 | + |
| 161 | +```python |
| 162 | +@app.activity_trigger(input_name="name") |
| 163 | +def say_hello(name: str) -> str: |
| 164 | + return f"Hello {name}!" |
| 165 | +``` |
| 166 | + |
| 167 | +### Orchestration API mapping |
| 168 | + |
| 169 | +| 1.x API | Native 2.x API | |
| 170 | +| --- | --- | |
| 171 | +| `context.get_input()` | The orchestrator's second argument | |
| 172 | +| `context.call_activity(name, input_)` | `ctx.call_activity(name, input=input_)` | |
| 173 | +| `context.call_activity_with_retry(...)` | `ctx.call_activity(..., retry_policy=policy)` | |
| 174 | +| `context.call_sub_orchestrator(name, input_)` | `ctx.call_sub_orchestrator(name, input=input_)` | |
| 175 | +| `context.call_sub_orchestrator_with_retry(...)` | `ctx.call_sub_orchestrator(...)` | |
| 176 | +| `context.task_all(tasks)` | `task.when_all(tasks)` | |
| 177 | +| `context.task_any(tasks)` | `task.when_any(tasks)` | |
| 178 | +| `context.new_guid()` | `ctx.new_uuid()` | |
| 179 | +| `df.EntityId(name, key)` | `entities.EntityInstanceId(name, key)` | |
| 180 | + |
| 181 | +Methods including `create_timer`, `wait_for_external_event`, |
| 182 | +`continue_as_new`, `set_custom_status`, `call_entity`, and `signal_entity` |
| 183 | +retain their names. Activity, sub-orchestration, and entity calls use keyword |
| 184 | +`input` instead of the 1.x `input_` or `operationInput` names. |
| 185 | +`continue_as_new` uses `new_input`. |
| 186 | + |
| 187 | +### Retry policy mapping |
| 188 | + |
| 189 | +`RetryOptions` accepted millisecond values. Native `RetryPolicy` uses |
| 190 | +`timedelta`: |
| 191 | + |
| 192 | +```python |
| 193 | +from datetime import timedelta |
| 194 | + |
| 195 | +from durabletask import task |
| 196 | + |
| 197 | +policy = task.RetryPolicy( |
| 198 | + first_retry_interval=timedelta(seconds=1), |
| 199 | + max_number_of_attempts=3, |
| 200 | + backoff_coefficient=2.0, |
| 201 | +) |
| 202 | + |
| 203 | +result = yield ctx.call_activity( |
| 204 | + "call_service", |
| 205 | + input=request, |
| 206 | + retry_policy=policy, |
| 207 | +) |
| 208 | +``` |
| 209 | + |
| 210 | +## Migrate client APIs |
| 211 | + |
| 212 | +`durable_client_input` supplies `DurableFunctionsClient` to coroutine functions |
| 213 | +and `SyncDurableFunctionsClient` to synchronous functions. Async client calls |
| 214 | +must be awaited; the corresponding sync calls must not be awaited. |
| 215 | + |
| 216 | +| 1.x client API | Native 2.x client API | |
| 217 | +| --- | --- | |
| 218 | +| `start_new` | `schedule_new_orchestration` | |
| 219 | +| `get_status` | `get_orchestration_state` | |
| 220 | +| `get_status_all` | `get_all_orchestration_states` | |
| 221 | +| `get_status_by` | `get_all_orchestration_states` with `OrchestrationQuery` | |
| 222 | +| `raise_event` | `raise_orchestration_event` | |
| 223 | +| `terminate` | `terminate_orchestration` | |
| 224 | +| `suspend` | `suspend_orchestration` | |
| 225 | +| `resume` | `resume_orchestration` | |
| 226 | +| `restart` | `restart_orchestration` | |
| 227 | +| `rewind` | `rewind_orchestration` | |
| 228 | +| `purge_instance_history` | `purge_orchestration` | |
| 229 | +| `purge_instance_history_by` | `purge_orchestrations_by` | |
| 230 | +| `read_entity_state` | `get_entity` | |
| 231 | +| `get_client_response_links` | `create_http_management_payload` | |
| 232 | + |
| 233 | +Native `restart_orchestration` reuses the current instance ID by default. Pass |
| 234 | +`restart_with_new_instance_id=True` to preserve the 1.x `restart` default. |
| 235 | + |
| 236 | +Native status APIs return durabletask models rather than the compatibility |
| 237 | +wrappers `DurableOrchestrationStatus`, `PurgeHistoryResult`, and |
| 238 | +`EntityStateResponse`. Update code that serializes or checks those return |
| 239 | +values. Use `get_orchestration_history` when history events are required. |
| 240 | + |
| 241 | +## Migrate durable entities |
| 242 | + |
| 243 | +One-argument entity functions remain available through |
| 244 | +`DurableEntityContext`. Native code can use a two-argument entity function or a |
| 245 | +`DurableEntity` class. A class exposes each method as an entity operation: |
| 246 | + |
| 247 | +```python |
| 248 | +from typing import Any |
| 249 | + |
| 250 | +import azure.durable_functions as df |
| 251 | +from durabletask.entities import DurableEntity |
| 252 | + |
| 253 | + |
| 254 | +@app.entity_trigger(context_name="context") |
| 255 | +class Counter(DurableEntity): |
| 256 | + def add(self, input_: Any = None) -> int: |
| 257 | + value = self.get_state(int, 0) + (input_ or 0) |
| 258 | + self.set_state(value) |
| 259 | + return value |
| 260 | + |
| 261 | + def get(self, input_: Any = None) -> int: |
| 262 | + return self.get_state(int, 0) |
| 263 | +``` |
| 264 | + |
| 265 | +Replace `df.EntityId` with `durabletask.entities.EntityInstanceId` at call |
| 266 | +sites. For function-based entities, return the operation result directly |
| 267 | +instead of calling `context.set_result`. |
| 268 | + |
| 269 | +## Review compatibility limitations |
| 270 | + |
| 271 | +- `DurableOrchestrationContext.histories` is unavailable. Retrieve history with |
| 272 | + `client.get_orchestration_history(instance_id)`. |
| 273 | +- The compatibility `show_history` and `show_history_output` status flags are |
| 274 | + accepted but ignored. |
| 275 | +- Distributed tracing is not yet wired through the Python provider. |
| 276 | +- Continuous history export is not supported by Azure Functions. |
| 277 | +- Unusual callable signatures can be misclassified by the compatibility layer. |
| 278 | + Keep compatibility orchestrators and entities to exactly one positional |
| 279 | + argument, and native functions to exactly two. |
| 280 | + |
| 281 | +See the [changelog](CHANGELOG.md) for the complete compatibility surface and |
| 282 | +known limitations. |
| 283 | + |
| 284 | +## Validate and deploy |
| 285 | + |
| 286 | +Before production rollout: |
| 287 | + |
| 288 | +1. Run unit tests against Python 3.13 or later. |
| 289 | +2. Run the app locally with Azurite and Azure Functions Core Tools. |
| 290 | +3. Start new orchestration, sub-orchestration, entity, retry, timer, and |
| 291 | + external-event scenarios used by the app. |
| 292 | +4. Verify status queries and HTTP management URLs. |
| 293 | +5. Test existing orchestration histories or drain existing instances. |
| 294 | +6. Deploy to a staging slot or non-production task hub before production. |
| 295 | + |
| 296 | +The [2.x samples](samples/) provide runnable native examples for common |
| 297 | +orchestration patterns. |
0 commit comments