Workflows
A workflow lets a client send many OData calls before saving. The server stores each change in staged state. It runs business logic on that staged state. The client can then see computed values, validation, and questions update in real time. Nothing is saved to main data until commit.
Example: a user enters quantity and product on a sales order. Business logic computes price and totals. In a workflow, these values update after each edit. Data is not saved to the main tables in the database until commit.
Create workflow → many OData calls (staged) → Commit
Durable and server-agnostic
Workflow state is durable. Staged changes, virtual entities, and pending questions are stored on the server and keyed by workflow id. They are not kept only in one server process.
This means:
- Edits survive deploy and restart. A new server can continue from stored state.
- Any server can handle any request. Each request includes workflow id and index. The server restores staged state and continues. Session affinity is optional for performance, not required for correctness.
flowchart LR
C[Client] -->|Workflow-Id + Workflow-Index| S1[Server A]
C -->|same headers| S2[Server B]
S1 <--> DB[(Durable staged state)]
S2 <--> DB
DB -->|commit| MAIN[(Committed data)]
Warm scopes
Durability does not require full reload on every request. If back-to-back requests for one workflow go to the same host, the server can keep that workflow scope warm. Then it can skip reading and rebuilding state again.
Correctness is the same in both modes. The server still writes staged fragments on every request. So after deploy, host change, or scope eviction, it can reload from durable state and continue.
This is controlled by WorkflowOptions.WarmWorkflowScopes. If you do not set it, the startup default is on in all environments except Development. Production uses this optimization by default. Development avoids accidental reliance on in-memory state that a deploy can remove. Set it explicitly to override this behavior.
Lifecycle
1. Create
POST /api/workflow
{
"message": "Workflow created successfully",
"workflowId": "a0d77a58-b208-46ce-b7f6-ddb03b89a6af"
}
Optionally tag it with ?stagingKey=<any-string> so a client that lost its in-memory state (restart,
another server) can re-find it later.
Resume by id or staging key
Because workflows are durable, a client with only an id can resume later. This includes ids from shared links or after restart.
The client can fetch resume info and continue from the last saved workflow step:
GET /api/workflow/{id}→{ "workflowId": ..., "nextIndex": 3 }GET /api/workflow/active?stagingKey=<key>→ the caller's active workflow with that key, or404
Send returned nextIndex as the next Workflow-Index. The .NET client wraps both flows:
AttachWorkflowAsync(id) resumes by id. This handle is temporary. Disposing that client does not close the workflow for its owner. The MCP server also resumes automatically by the agent staging key.
2. Make changes
Send GET, PATCH, or POST calls with these headers:
Workflow-Id: a0d77a58-b208-46ce-b7f6-ddb03b89a6af
Workflow-Index: 1
Increment Workflow-Index on each call. To return updated data from PATCH, use $select and $expand. Each response returns user prompts in one @workflow.userprompts envelope. This envelope includes both messages and questions.
3. Commit
POST /api/workflow/commit with the same headers. The server applies every staged change as one operation. The workflow stays open afterwards, so the client can keep editing and commit again.
Commit is successful only when data is saved. Result details are always returned in @workflow.userprompts. The workflow stays open in all cases:
| Result | Status | Meaning |
|---|---|---|
| Committed | 200 |
All staged changes were saved. |
| Question pending | 422 |
A question must be answered first. Nothing saved, but the question is kept — answer it (see below), then commit again. |
| Rejected | 400 |
Save-time validation failed. Fix the data, then commit again. |
A question can appear for the first time at commit. This happens when a save-time rule runs only during saving. In that case, commit is held with 422 instead of 200.
Virtual entities also participate in workflows. They are staged with other changes and committed as one operation. See Virtual Entities › Workflows.
Questions
Business logic can ask the user a question. Example: "You do not have enough inventory. Ship anyway?" Questions need user input, so they appear only in workflows. In plain requests, each question uses its default answer. A question still stages its related change, waits for an answer, and blocks commit until answered. See DataSet Workflows for client API details.
Answering a question
Each question choice includes a continuation link. This lets a client answer without hardcoding an answer route:
href— the path to invoke to select this choice.method— the HTTP verb to use (alwaysPOST).
"choices": [
{ "key": "accept", "label": "Continue", "href": "/api/userprompts/{promptId}/answer/accept", "method": "POST" },
{ "key": "reject", "label": "Cancel", "href": "/api/userprompts/{promptId}/answer/reject", "method": "POST" }
]
Call href with the choice method, and include the same Workflow-Id and Workflow-Index headers used in other workflow requests. Workflow identity is not in the URL. Request body is not needed because choice is in the path. This link is the only supported way to answer a question.
Messages (prompts with no choices) carry no choices, so they have no links.
Response envelopes
There is one envelope format for user prompts. It carries messages (no choices) and questions (with choices). A plain request runs without user interaction, so it returns messages only and applies default answers for questions.
| Context | Envelope |
|---|---|
| Plain request | @userprompts (messages only; question defaults taken) |
| Workflow request | @workflow.userprompts (messages and questions) |
Envelope content is grouped by source: global, {entityGuid}, or {entityGuid}.{propertyName}. In each group, a prompt with no choices is a message. A prompt with choices is a question.
Index advancement
Every workflow request must include Workflow-Index. The server enforces this value. It is not optional tracking data. A request with no index is rejected with 400. A request with an unexpected index (reused, skipped, or out of order) is rejected with 409 and includes Workflow-Next-Index for resync.
Workflow-Index advances only when a request becomes a durable step. A durable step changes stored workflow state that later requests use:
- Advances — every
2xxrequest (staged edit or successful commit), and a422commit (staged state and unanswered question were stored, so you can answer and commit again). - Does not advance — a
400rejection or any other failure (for example, failed read). In these cases, nothing was stored.
The server is the final source for index values. It returns the next expected index in the Workflow-Next-Index response header. The client increases its index after a step that advanced, and reuses the same index after a step that did not. 409 Conflict means out-of-order or cross-instance delivery. The response includes Workflow-Next-Index so the client can resync and retry.
Lifetime
A workflow closes when the client disposes its connection, or when TTL expires for abandoned workflows. Closing discards all uncommitted staged state.