Route requests with a dispatcher

A dispatcher receives an event, makes a typed decision, and queues the child workflow that should handle it. Set type: dispatcher on the routing workflow; keep its ordinary trigger, such as a Slack webhook. Each child declares the dispatcher it belongs to.

Decision and dispatch steps were introduced in CLI 0.56.0. Use CLI 0.56.0 or newer and a compatible managed runtime when deploying these examples.

Route Slack mentions

Create three separate workflow files. Sync all three to the same workspace, with the exact names shown below. The dispatcher receives mentions and chooses between support and engineering; an unrelated request starts neither child.

Dispatcher: slack-intake

apiVersion: outcomeci.workflow/v1
name: slack-intake
type: dispatcher
 
trigger:
  webhook: {uses: slack, auth: secrets.slack_signing, events: [mention]}
 
secrets:
  slack_signing: vault:slack/signing-secret
 
reasoning:
  route: {model: openai/gpt-6-luna}
 
steps:
  - classify:
      with: trigger
      using: route
      decision:
        intent:
          type: choice
          instructions: Choose the team best suited to the Slack request.
          choices:
            - {value: support, description: Product help or usage questions}
            - {value: engineering, description: Bug reports or requests to change code}
            - {value: none, description: Anything outside these teams' work}
 
  - support:
      when: classify.intent.choice == "support"
      dispatch: support-answer
      with: trigger
 
  - engineering:
      when: classify.intent.choice == "engineering"
      dispatch: engineering-triage
      with: trigger

Grant the signing secret to slack-intake. Enable its webhook in the dashboard and use that URL as the Slack app's event request URL. Slack verification, mention filtering, and event deduplication use the existing provider webhook receiver.

Child: support-answer

apiVersion: outcomeci.workflow/v1
name: support-answer
trigger: {type: dispatcher, dispatcher: slack-intake}
 
secrets:
  slack: vault:slack/bot-token
 
apis:
  slack: {uses: slack, auth: secrets.slack}
 
steps:
  - answer:
      from: trigger
      reason: >
        Read the support question and reply in its Slack thread. If the answer
        is not clear, ask one specific follow-up question.
      can:
        - slack.post: {channel: trigger.channel, thread_ts: trigger.ts}

Child: engineering-triage

apiVersion: outcomeci.workflow/v1
name: engineering-triage
trigger: {type: dispatcher, dispatcher: slack-intake}
 
secrets:
  slack: vault:slack/bot-token
 
apis:
  slack: {uses: slack, auth: secrets.slack}
 
steps:
  - triage:
      from: trigger
      reason: >
        Read the engineering request and reply in its Slack thread with a
        concise summary and any missing information needed to investigate it.
      can:
        - slack.post: {channel: trigger.channel, thread_ts: trigger.ts}

Grant the bot-token secret separately to each child. Children own their own API grants, model settings, and credentials; the dispatcher's permissions are not inherited. The dispatcher needs only the signing secret for this example. The with: trigger input becomes each child's trigger, preserving the Slack channel, user, text, and ts used to reply in the original thread.

Keep groups separate

A child's dispatcher is the exact name of its parent workflow in the same workspace. Bare trigger: dispatcher is invalid. For example, two independent groups can coexist:

Dispatcher name Children Each child's trigger
slack-intake support-answer, engineering-triage {type: dispatcher, dispatcher: slack-intake}
operations-intake incident-triage, access-request {type: dispatcher, dispatcher: operations-intake}

Give operations-intake its own ordinary ingress trigger and type: dispatcher. Its children explicitly name operations-intake; slack-intake cannot invoke those children. Missing or ambiguous target names and mismatched dispatcher membership fail without starting the child.

Only dispatcher workflows may contain decision or dispatch steps. Ordinary child workflows can use the existing agent, model, await, and converse steps. A dispatch target is a literal child workflow name, fixed in the parent's workflow definition. A model chooses typed answers, and the existing when conditions determine which declared targets run.

Decisions and results

A decision mapping contains named questions. Each question has type and instructions:

Question type Additional declaration Result
predicate None A probability
choice choices: [{value, description?}, ...] A selected choice, probabilities, and confidence
score levels: [{label, description?}, ...] A numeric score, probabilities, and confidence

Choice values are strings or booleans. A question's name becomes its output key: classify.intent.choice reads the answer to intent in the example. Each answer includes its question name and type. Answers retain the provider's typed fields; they do not promise a free-text explanation. A decision step has no agent workspace or tools. It accepts with input references, using for a model profile, and the existing optional when condition.

Use openai/gpt-6-luna or a typesafe/ model profile for a decision. OpenAI currently supports only gpt-6-luna on the Decisions endpoint; other OpenAI models are rejected during workflow validation. To use Jev, declare an explicitly granted Vault API key:

secrets:
  typesafe: vault:typesafeai/api-key
 
reasoning:
  route: {model: typesafe/jev-latest, key: secrets.typesafe}

Create this key under the Vault service typesafeai; the typesafe service name is also accepted. The model ID keeps the LiteLLM typesafe/ prefix regardless of which Vault service name you use. Grant that secret to the dispatcher. TypeSafe models are available for decision steps; they are not supported as chat/model steps or policy reviewers. OpenAI profiles use the existing key: secrets.<name> option, or the cloud's metered provider key when no key is declared.

Dispatch is asynchronous

Each dispatch step takes one with reference and queues one child run. Its result contains run_id, workflow_id, workflow_revision_id, and status: queued. The parent continues immediately; a completed parent does not mean its children have completed successfully. Inspect each child run for its own outcome.

Use several conditional dispatch steps to fan out to several children. When no condition matches, no child is queued. Repeating the same accepted dispatch operation returns the original child receipt; changing its input conflicts. The accepted child revision stays pinned. Cycles and excessive nesting are rejected. A local run cannot queue children without a managed dispatch client.

Decision answers and dispatch receipts are retained in the parent's run artifacts. policy.json continues to record permission reviews and grant denials separately.