# Workflows

> Tracon documentation. Tracon is a .NET package family that adds a control
> plane on Microsoft Agent Framework, published as NuGet packages that run
> inside your own ASP.NET Core application.
>
> Page: https://tracon.dev/http-api/workflows/
> Summary: The workflows endpoints of the Tracon HTTP API — 16 operations.

16 operations. `{prefix}` is the route prefix passed to
`MapTracon`; the template uses `/tracon`.

## GET {prefix}/api/workflows

**Operation ID:** `TraconListWorkflows`

**Lists workflows defined in code and stored in the database.**

What the list contains depends on whether the workflow engine is registered. With the engine, both code-defined and stored workflows appear, because only the engine can see the ones built in code. Without it, only stored definitions are listed — managing definitions does not require the engine, but running them does.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowDescriptor`](/http-api/schemas/workflowdescriptor/) | — |

## GET {prefix}/api/workflows/{name}

**Operation ID:** `TraconGetWorkflow`

**Returns a single workflow definition.**

Only stored definitions are editable and only they are returned here. A code-defined workflow is listed and can be run but has no stored definition, so it answers 404 with a distinct 'No editable definition' title — different from the plain not-found title used for a name that does not exist at all. Read the structure of a code-defined workflow from the graph endpoint instead.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → [`WorkflowDefinition`](/http-api/schemas/workflowdefinition/) | — |

## PUT {prefix}/api/workflows/{name}

**Operation ID:** `TraconSaveWorkflow`

**Creates or updates a workflow definition.**

The definition is validated at save time with the same rules the compiler applies, so a shape that could not run is rejected with 400 instead of failing on the first run. Which fields are required depends on the kind — a manager driven workflow needs its manager agent, for example. A function node's 'settings' must be a JSON object that fits the settings type its function declares (see settingsSchema on GET /api/workflows/functions): an unknown field, a missing required member and a value of the wrong type are all refused with a message that names the node and the member, and so is any settings on an agent node or on a function that declares none. Settings are stored in the definition: never put a secret in them, name its configuration key instead. A node's 'timeout' (function or agent node, hh:mm:ss) must be from 1 ms to about 49.7 days, 30 seconds less on an agent node; it is not checked against the run's timeout. An agent node's 'agentVersion' pins the step to that version of the agent; it must be 1 or more, only an agent node may carry one, and the agent must have that version (an agent defined in code keeps no versions): anything else is refused with 400. 'runTimeout' bounds one run of this workflow: it may only shorten Tracon:Workflows:RunTimeout unless the host sets Tracon:Workflows:MaxRunTimeout, and a value above that ceiling is refused with 400. The call replaces the whole definition: omitted fields are cleared, not merged. The name comes from the path and is not taken from the body. The name 'functions' is reserved (400): GET /api/workflows/functions uses it. Every save appends a version rather than overwriting; the previous content stays readable through GET /api/workflows/{name}/versions.

**Authorization:** bearer authentication; `Admin` role policy when that policy is registered; `WorkflowsAdmin` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

**Request body** (required):

- `application/json` → [`WorkflowSaveRequest`](/http-api/schemas/workflowsaverequest/)

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → [`WorkflowDefinition`](/http-api/schemas/workflowdefinition/) | — |

## DELETE {prefix}/api/workflows/{name}

**Operation ID:** `TraconDeleteWorkflow`

**Deletes a workflow definition.**

Only a stored definition can be deleted; a code-defined workflow is removed by changing the application, and asking for one here returns 404. Runs and checkpoints already recorded are kept, so past executions stay readable, but a checkpoint cannot be resumed once the definition it needs is gone. The version history is kept too, so a version number is never given to a second content: saving the name again continues the numbering.

**Authorization:** bearer authentication; `Admin` role policy when that policy is registered; `WorkflowsAdmin` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

| Response | Body | Headers |
|---|---|---|
| **204** No Content | — | — |

## GET {prefix}/api/workflows/{name}/graph

**Operation ID:** `TraconGetWorkflowGraph`

**Returns the compiled graph of the workflow.**

Node IDs are identical to the executor IDs in run events; this is how the UI colors nodes live. The response also carries the Mermaid text generated by Microsoft Agent Framework.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → [`WorkflowGraph`](/http-api/schemas/workflowgraph/) | — |

## POST {prefix}/api/workflows/{name}/rollback

**Operation ID:** `TraconRollbackWorkflow`

**Writes a workflow definition as a new version with the content of a previous version.**

A rollback moves forward, not backward: the old content is appended as a NEW version and the history is never rewritten, so the rollback itself stays auditable ('workflow.rollback') and can be rolled back in turn. The content is checked the way a save is: a version that names a function that is no longer registered, or an agent version that no longer exists, is refused with 400 and nothing is written. An unknown version number returns 404; a workflow defined in code returns 409. A run that is waiting keeps the version it recorded, so a rollback does not change what it resumes with.

**Authorization:** bearer authentication; `Admin` role policy when that policy is registered; `WorkflowsAdmin` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

**Request body** (required):

- `application/json` → [`WorkflowRollbackRequest`](/http-api/schemas/workflowrollbackrequest/)

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → [`WorkflowDefinition`](/http-api/schemas/workflowdefinition/) | — |
| **400** Bad Request | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **409** Conflict | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## POST {prefix}/api/workflows/{name}/run

**Operation ID:** `TraconRunWorkflow`

**Runs the workflow and streams its events over SSE, or queues it with 'Prefer: respond-async'.**

Each frame carries a RunEvent. The first frame reports the run ID; every agent invoked within the workflow opens its own runs row, viewable as a tree via GET /api/runs/{runId}/tree. If a registered IRunAuthorizationHandler denies the caller, the run does not start and a 403 is returned, before the quota check. When session ownership is turned on, naming another user's session in 'sessionId' is refused the same way, and so is opening a NEW session when no authenticated identity can be resolved to own it. A streamed run belongs to the connection: if the client stops reading, the run closes as Canceled. A request carrying 'Prefer: respond-async' is checked the same way and then queued: the answer is '202 Accepted' with a 'Location' header and the run id, the run executes in the background worker, and its events are read from GET /api/runs/{runId}/events. A queued run does not depend on any connection, and with ResumeInterruptedRuns on it is resumed from its latest checkpoint after a restart. 'Idempotency-Key' is accepted only together with 'Prefer: respond-async' (400 otherwise); a repeated request returns the same 202 and queues nothing. With queuing disabled (Tracon:AsyncRun:Enabled = false) the preference is answered with 501.

**Authorization:** bearer authentication; `Operator` role policy when that policy is registered; `RunsWrite` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

**Request body** (optional):

- `application/json` → [`WorkflowRunHttpRequest`](/http-api/schemas/workflowrunhttprequest/)

| Response | Body | Headers |
|---|---|---|
| **200** OK | `text/event-stream` → `string` | — |
| **202** Accepted | `application/json` → [`AcceptedRunResponse`](/http-api/schemas/acceptedrunresponse/) | — |
| **400** Bad Request | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **403** Forbidden | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **429** Too Many Requests | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **501** Not Implemented | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **503** Service Unavailable | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## GET {prefix}/api/workflows/{name}/versions

**Operation ID:** `TraconListWorkflowVersions`

**Lists a workflow definition's version history, newest first.**

Every entry is a full definition snapshot, not a delta, so a single entry is enough to inspect or restore a past state. The workflow must have a current stored definition; a deleted name returns 404 even though its versions are kept (a version number is never reused). A workflow defined in code also returns 404, with a reason that says so: it is not stored by Tracon and has no version history. The 'agentVersion' of a run's row is the workflow version it was compiled from.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowDefinition`](/http-api/schemas/workflowdefinition/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## GET {prefix}/api/workflows/{name}/versions/{a}/diff/{b}

**Operation ID:** `TraconGetWorkflowVersionDiff`

**Returns two definition versions as raw JSON; the diff is computed in the UI.**

The server does no diffing and takes no position on how a change should be displayed; it returns both snapshots verbatim as 'left' and 'right' so the client chooses the presentation. The two version numbers may be given in any order. When either version is missing the response is 404 and names the one that was not found; a workflow defined in code returns 404 saying it has no version history.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `name` | path | yes | `string` | — |
| `a` | path | yes | `integer (int32)` | pattern `^-?(?:0\\|[1-9]\d*)$` |
| `b` | path | yes | `integer (int32)` | pattern `^-?(?:0\\|[1-9]\d*)$` |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → [`WorkflowVersionDiffResponse`](/http-api/schemas/workflowversiondiffresponse/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## GET {prefix}/api/workflows/functions

**Operation ID:** `TraconListWorkflowFunctions`

**Lists function nodes registered in code.**

Function nodes are defined only in code, with AddWorkflowFunction - the same code-only boundary AddTool draws for tools. This endpoint does not offer a write path; the console picks from this list when an operator builds a Sequential workflow's node list. A function that takes settings carries settingsSchema, a JSON schema of the object a node of it may carry in 'settings'; a function that takes none has settingsSchema null and its nodes must carry no settings. 'timeout' is the time limit the function was registered with, which a node's own 'timeout' replaces; null means none. The schema is for display: saving the workflow does the checking. An empty list means no function was registered, or the workflow engine was never turned on with UseWorkflows - either way, existing agent-only workflows are unaffected.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `WorkflowsRead` API-key scope.

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowFunctionResponse`](/http-api/schemas/workflowfunctionresponse/) | — |

## GET {prefix}/api/workflows/requests

**Operation ID:** `TraconListWorkflowInbox`

**Lists the human input requests the tenant's workflow runs are waiting on.**

One item per request, oldest first. The same request id in the same session is ONE request even when several runs hold it (an answered run stays 'AwaitingInput' and the continuation republishes the requests that are still open); the item names the newest run that waits on it, which is the run to answer. 'state' is 'pending' (the default: nobody has answered), 'answered' (answered, but the run still waits) or 'all'; 'workflowName' narrows the list to one workflow. The filter runs before the page, so 'skip' and 'take' (1-200, default 50) page over the filtered list. Each item carries 'expiresAt' (when the workflow sets a request timeout), 'answered' and 'lastResponseRunId'. A run whose request expired is closed and leaves the list. The list is the tenant's: a registered IRunAuthorizationHandler that denies reading runs rejects it with 403 and never filters it, so a page is never shorter than the caller is allowed to see. An installation that has not registered the workflow engine answers 501. Runs that started waiting before this list existed are not in it; read them with GET /api/workflows/runs/{runId}/requests.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `RunsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `state` | query | no | `string` | — |
| `workflowName` | query | no | `string` | — |
| `skip` | query | no | `integer (int32)` | pattern `^-?(?:0\\|[1-9]\d*)$` |
| `take` | query | no | `integer (int32)` | pattern `^-?(?:0\\|[1-9]\d*)$` |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowPendingRequest`](/http-api/schemas/workflowpendingrequest/) | — |
| **400** Bad Request | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **403** Forbidden | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **501** Not Implemented | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## GET {prefix}/api/workflows/runs/{runId}/checkpoints

**Operation ID:** `TraconListWorkflowCheckpoints`

**Lists the checkpoints of a workflow run.**

Checkpoints are the points a run can be resumed from; each entry's id is what the resume endpoint takes. A run belonging to another tenant is reported as 404 rather than 403, so the API does not confirm that it exists. An empty list means the run wrote no checkpoint — checkpointing is a property of how the workflow was built, not something this endpoint can turn on. Checkpoints are subject to retention, so an old run may have none left. A denial by a registered IRunAuthorizationHandler produces the same 404.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `RunsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `runId` | path | yes | `string (uuid)` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowCheckpointRecord`](/http-api/schemas/workflowcheckpointrecord/) | — |

## GET {prefix}/api/workflows/runs/{runId}/requests

**Operation ID:** `TraconListWorkflowRequests`

**Lists a run's pending human input requests.**

Only a run in the 'AwaitingInput' state returns requests. The prompt and the form are read from the run's event stream; 'expiresAt', 'answered' and 'lastResponseRunId' come from the inbox row the run wrote when it started waiting (see GET /api/workflows/requests). A run belonging to another tenant, and a denial by a registered IRunAuthorizationHandler, both return the same 404.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `RunsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `runId` | path | yes | `string (uuid)` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowPendingRequest`](/http-api/schemas/workflowpendingrequest/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## POST {prefix}/api/workflows/runs/{runId}/respond

**Operation ID:** `TraconRespondWorkflowRequest`

**Responds to a pending request and resumes the run, streamed or queued with 'Prefer: respond-async'.**

The response is matched to the request re-published with the same ID in the execution resumed from the checkpoint. Resuming opens a NEW runs row; events stream over SSE — so this STARTS a run, and a registered IRunAuthorizationHandler is asked with the source run's id; a denial returns 403 before any run row is opened. The answer is checked before the stream opens: a response of the wrong shape, a run that is not waiting and an unknown request id are all 400; an unknown run is 404. The run continues with the version of the workflow's definition it recorded, or with the stored version 'definitionVersion' names (a version the workflow does not have is 400). With 'Prefer: respond-async' it is then queued: the reply is '202 Accepted' and the run continues in the background worker, so a machine that answers a request does not have to keep a connection open. The queued answer is stored with the job until the job is removed; do not put a secret in it. 'Idempotency-Key' is accepted only together with 'Prefer: respond-async'. Every answer that starts a run is written to the audit trail as 'workflow.respond' before the run starts, with a SHA-256 of the answer and never the answer itself; when that entry cannot be written the answer is refused with 500 and nothing is started. A workflow whose responseMode is 'SingleAnswer' takes one answer per request: repeating the same answer (approved, text and data; the checkpoint and version do not count) starts nothing and follows the run the first one started - the same SSE frames read from the stored run, or the same 202 with 'Prefer: respond-async' - and a different answer is 409, which names neither the run nor the other answer. An answer whose run failed or was cancelled before the workflow received it can be given again and starts a new run. With 'Branch', the default, every answer opens a new run. A request whose 'form' is 'ToolApproval' is an agent's tool call that waits for approval: answer it with 'approved', a reason in 'text' that the model reads, and 'remember' to also save the approval as a rule for that agent and tool. It takes no 'data': the tool call comes from the request the workflow recorded, so an answer cannot approve a different one. The decision is audited as 'approval.decision' before it is applied, and 'remember' with 'approved' false, or on any other form, is 400.

**Authorization:** bearer authentication; `Operator` role policy when that policy is registered; `RunsWrite` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `runId` | path | yes | `string (uuid)` | — |

**Request body** (optional):

- `application/json` → [`WorkflowRespondHttpRequest`](/http-api/schemas/workflowrespondhttprequest/)

| Response | Body | Headers |
|---|---|---|
| **200** OK | `text/event-stream` → `string` | — |
| **202** Accepted | `application/json` → [`AcceptedRunResponse`](/http-api/schemas/acceptedrunresponse/) | — |
| **400** Bad Request | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **403** Forbidden | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **409** Conflict | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **501** Not Implemented | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **503** Service Unavailable | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## GET {prefix}/api/workflows/runs/{runId}/responses

**Operation ID:** `TraconListWorkflowResponses`

**Lists the answers given to a run's human input requests.**

One entry per answer that started a run, oldest first: the request it answered, the run it started (responseRunId), who answered and when, and a SHA-256 of the answer (answerHash) - never the answer itself. The hash tells two answers apart; it does not hide an answer with few possible values, such as a yes or no. 'claimScope' is set when the workflow takes one answer per request; 'status' is Accepted until the workflow received the answer (Delivered), and Abandoned for an answer whose run stopped and was started again. A run belonging to another tenant, and a denial by a registered IRunAuthorizationHandler, both return the same 404.

**Authorization:** bearer authentication; `Reader` role policy when that policy is registered; `RunsRead` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `runId` | path | yes | `string (uuid)` | — |

| Response | Body | Headers |
|---|---|---|
| **200** OK | `application/json` → array of [`WorkflowResponseRecord`](/http-api/schemas/workflowresponserecord/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |

## POST {prefix}/api/workflows/runs/{runId}/resume

**Operation ID:** `TraconResumeWorkflow`

**Resumes from a checkpoint and streams events over SSE, or queues it with 'Prefer: respond-async'.**

Resuming opens a NEW run rather than continuing the old one: the original run row is never rewritten, and the first streamed frame reports the new run id. The body is optional — without a checkpoint id the run resumes from its latest checkpoint. The run resumes with the version of the workflow's definition that it recorded ('agentVersion' of its row), so an edit made while it waited changes nothing; 'definitionVersion' names another stored version, and a version the workflow does not have is 400, never a silent fall-back to the current one. A version whose graph differs from the checkpoint's fails the run with 'the graph's structure differs'. A run that recorded no version resumes with the current definition. The engine must be registered; otherwise the response is 501. The source run and its checkpoint are checked before the stream opens: an unknown run is 404, and a run that is not a workflow run or has no such checkpoint is 400. A failure after the stream opens arrives as an SSE error frame. Resuming STARTS a run, so a registered IRunAuthorizationHandler is asked with the SOURCE run's id; a denial returns 403 before any run row is opened. With 'Prefer: respond-async' the resume is queued and answered with '202 Accepted', like a queued run, after the same checks. A queued resume is not counted against the quota, the same as a streamed one. 'Idempotency-Key' is accepted only together with 'Prefer: respond-async'.

**Authorization:** bearer authentication; `Operator` role policy when that policy is registered; `RunsWrite` API-key scope.

| Parameter | In | Required | Type | Description and rules |
|---|---|---|---|---|
| `runId` | path | yes | `string (uuid)` | — |

**Request body** (optional):

- `application/json` → [`WorkflowResumeHttpRequest`](/http-api/schemas/workflowresumehttprequest/)

| Response | Body | Headers |
|---|---|---|
| **200** OK | `text/event-stream` → `string` | — |
| **202** Accepted | `application/json` → [`AcceptedRunResponse`](/http-api/schemas/acceptedrunresponse/) | — |
| **400** Bad Request | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **403** Forbidden | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **404** Not Found | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **501** Not Implemented | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
| **503** Service Unavailable | `application/problem+json` → [`ProblemDetails`](/http-api/schemas/problemdetails/) | — |
