> ## Documentation Index
> Fetch the complete documentation index at: https://yukta.infenia.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Write a workflow supervisor

> Make a supervisor out of a workflow in an internal session: SUPERVISOR_SIGNAL in, SUPERVISOR_DECISION out, started for each event you list.

<dl />

**Use this when** the decision should be visible and editable as a flowchart, and may use any node: a `BRANCH`, an `HTTP_REQUEST`, an `LLM_CALL` or a decision plugin. For each event you list, the server starts the workflow with the event and reads how it ended.

**Prerequisites:** [Ask for input](https://yukta.infenia.com/guides/ai/ask-for-input.md) (the `approvals` session) and [Assign a supervisor](https://yukta.infenia.com/guides/supervise/assign-a-supervisor.md). Defining a session and a supervisor needs an `ADMIN` key; add `-H "X-API-Key: $YUKTA_API_KEY"` to every `curl` if your server has keys. This guide follows [example 17](https://github.com/infenia/yukta/tree/main/examples/17-supervisor-workflow).

## Steps

**1. Write the decision workflow in an internal session**

A session tagged `"yukta.internal": "true"` is **internal**: hidden from the default lists, and its own events never reach a supervisor. The workflow begins at `SUPERVISOR_SIGNAL` and ends in `SUPERVISOR_DECISION` nodes. Save as `session.json`:

```json session.json theme={"system"}
{
  "sessionId": "supervisors",
  "name": "Supervisor workflows",
  "tags": { "yukta.internal": "true" },
  "workflows": {
    "low-risk-answers": {
      "workflowId": "low-risk-answers",
      "description": "Approve a LOW risk request; hand anything else up",
      "nodes": [
        { "nodeId": "trigger", "type": "SUPERVISOR_SIGNAL", "config": {} },
        {
          "nodeId": "gate",
          "type": "BRANCH",
          "config": {
            "mode": "EXPRESSION",
            "cases": {
              "payload.request.spec.risk == 'LOW'": "low",
              "payload.request.spec.risk != 'LOW'": "other"
            },
            "strictMode": true
          }
        },
        {
          "nodeId": "approve",
          "type": "MAPPER",
          "config": {
            "mode": "PROJECTION",
            "mapping": {
              "commands": "{ {type: 'AnswerInput', requestId: payload.request.id, decision: 'approve', rationale: 'low risk, approved by the supervisor workflow', confidence: 1.0} }"
            }
          }
        },
        { "nodeId": "answered", "type": "SUPERVISOR_DECISION", "config": { "outcome": "SUCCESS", "reason": "low risk" } },
        { "nodeId": "hand-up", "type": "SUPERVISOR_DECISION", "config": { "outcome": "ESCALATE", "reason": "not low risk; a person or a supervisor above should decide" } }
      ],
      "edges": [
        { "source": "trigger", "target": "gate" },
        { "source": "gate", "target": "approve", "sourcePort": "low" },
        { "source": "gate", "target": "hand-up", "sourcePort": "other" },
        { "source": "approve", "target": "answered" }
      ]
    }
  }
}
```

The signal's payload holds the event, with secrets masked, and for an input request the `request` (`id`, `spec.risk`, `spec.kind`, `spec.prompt`...). How the run ends decides what happens:

| Ends with | Effect |
| - | - |
| `SUCCESS` and `{"commands": [...]}` | Sends those commands (at most 10 per run) |
| `ESCALATE` | Hands the request in the event up, with the reason as advice |
| `RETRY` | Runs the workflow once more |
| A failure, a timeout, or an unreadable result | No opinion |

A command the contract cannot read fails the run instead of being dropped. `AnswerInput`, `EscalateInput`, `PauseWorkflow`, `ResumeWorkflow`, `PauseNode`, `ResumeNode` and `SetPriority` can always be mapped; `StopWorkflow`, `StopNode`, `SkipNode`, `Restart`, `RestartFromNode`, `SetLimits` and `SetBreakpoint` only when `allowedCommands` names them.

```bash theme={"system"}
curl -s -X POST localhost:9858/api/sessions -H 'Content-Type: application/json' -d @session.json
```

**2. Save the supervisor**

```json supervisor.json theme={"system"}
{
  "id": "low-risk-workflow",
  "type": "workflow",
  "settings": {
    "sessionId": "supervisors",
    "workflowId": "low-risk-answers",
    "events": ["InputRequested"],
    "maxConcurrent": 2,
    "timeout": 30,
    "allowedCommands": ["AnswerInputCommand", "EscalateInputCommand"],
    "canAnswer": true,
    "maxRisk": "LOW"
  }
}
```

| Setting | Meaning |
| - | - |
| `sessionId`, `workflowId` | The internal session and its workflow; must exist when the supervisor is saved |
| `events` | Event types that start it, such as `InputRequested` |
| `maxConcurrent` | Runs at once, default 2 |
| `timeout` | Seconds one run may take, 1 to 600, default 60 |
| `when` | Optional read-only SpEL condition over the event, such as `request.spec.risk == 'LOW'`; the run starts only when it holds |
| `allowedCommands`, `canAnswer`, `maxRisk` | As for any supervisor. A workflow supervisor may send **no** command until `allowedCommands` names some |

```bash theme={"system"}
curl -s -X POST localhost:9858/api/supervisors -H 'Content-Type: application/json' -d @supervisor.json
```

**3. Give it something to own**

```bash theme={"system"}
curl -s -X PUT localhost:9858/api/supervisors/low-risk-workflow/assignments \
  -H 'Content-Type: application/json' -d '[{"target":"session:approvals"}]'
```

The supervisor answers only requests of what it owns, and never one raised by a run it started.

**4. Trigger a request**

Start the `deploy` workflow from [Ask for input](https://yukta.infenia.com/guides/ai/ask-for-input.md), whose request has risk `LOW`:

```bash theme={"system"}
curl -s -X POST localhost:9858/api/workflow/start -H 'Content-Type: application/json' \
  -d '{"sessionId":"approvals","workflowId":"deploy","payload":{"build":"42","environment":"staging"}}'
```

## Verify it worked

The run goes on without anyone answering, and ends in the `deployed` terminal.

```bash theme={"system"}
curl -s "localhost:9858/api/sessions/approvals/inputs?state=ANSWERED"
curl -s "localhost:9858/api/supervisors/decisions?supervisor=low-risk-workflow"
curl -s "localhost:9858/api/sessions?includeInternal=true"
```

The request is `ANSWERED` by the supervisor with the rationale from the workflow, the decision shows outcome `applied`, and the `supervisors` session appears only with `includeInternal=true`. Change the request to risk `MEDIUM` and start again: the workflow ends with `ESCALATE`, the request becomes `ESCALATED` with your reason as advice, and it waits for the next tier or a person. A `HIGH` request is never answered by a supervisor, whatever the workflow says.

## Variations

* **A section supervisor.** `section-supervisor.json` in the example owns only `workflow:my-session/my-workflow//build.*`, runs a one-node `section-triage` workflow that always ends `ESCALATE`, and reports to `low-risk-workflow` via `"parents"`: [Build a hierarchy](https://yukta.infenia.com/guides/supervise/build-a-hierarchy.md).
* **Make the decision with a model.** Put an `LLM_CALL`, or an `MCP_TOOL_CALL` that reads other runs with a `VIEWER` key, in front of the decision node ([Call an LLM](https://yukta.infenia.com/guides/ai/call-an-llm.md)).
* **Observing internal sessions.** Events of internal sessions never reach a supervisor unless its settings set `observeInternal: true` and it owns that session. Supervisors nested deeper than `yukta.supervisor.max-depth` (2) are refused.
* **Their own slots.** Supervisor runs hold one of `yukta.supervisor.reserved-slots` (4) slots, so a saturated server still runs them and they cannot starve normal work. A run that finds none within its timeout is skipped.
* **Trusted authors.** Only admins can define sessions, and a workflow can call any endpoint it holds a key for, so write these workflows as trusted code.

## Related

* [Supervisor engines reference](https://yukta.infenia.com/reference/supervisors/engines.md) for the `workflow` engine.
* [Supervisor contract reference](https://yukta.infenia.com/reference/supervisors/contract.md).
* [Terminal plugins](https://yukta.infenia.com/reference/plugins/terminals.md) for `OUTCOME_TERMINAL`, whose exits `SUPERVISOR_DECISION` shares.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.