Skip to main content

Declarative Workflows

Define a multi-agent orchestration in YAML instead of code. A declarative workflow is a Microsoft Agent Framework Declarative Workflow whose execution graph -- sequential steps, conditionals, loops, foreach, and agent invocations -- is compiled from a file instead of assembled by hand. Workflows are managed alongside Declarative Agents and are told apart by the top-level kind field: kind: Prompt is an agent, kind: Workflow is a workflow.

The important rule is the same as for declarative agents: the YAML is a specification, and ChatWalaʻau owns construction. A workflow only references agents by name -- it never carries credentials, a provider, or sampling parameters. Each referenced agent is built by ChatWalaʻau exactly as it would be on its own, so its model, options, and per-agent tools come from that agent's spec.

What you get

  • Compile YAML to a real workflow graph. Drop a kind: Workflow YAML into your DECLARATIVE_AGENTS_DIR (the same folder as your agents) and ChatWalaʻau compiles it with the Microsoft Agent Framework WorkflowFactory. There is no separate directory or enable flag -- leave the folder unset and there are simply no workflows.
  • Orchestrate your declarative agents. A workflow's InvokeAzureAgent steps call your kind: Prompt agents (including the built-in CORE agent). ChatWalaʻau resolves and builds each one through its own agent/provider path, so credentials and model routing stay in ChatWalaʻau's hands. InvokeAzureAgent resolves only to a declarative Prompt agent -- it never constructs a raw Foundry agent.
  • The full action surface. All 23 Microsoft Agent Framework declarative action kinds are authorable, grouped by category (see below): variables, control flow, output, agent, tool, HTTP, human-in-the-loop, and workflow control.
  • Managed alongside agents. Prompt agents and workflows live in one management modal (the robot icon in the sidebar footer), told apart by a Prompt / Workflow tag. Activate a Prompt agent, or select a workflow to run in chat -- the choice is remembered.
  • Two ways to run it.
    • In chat -- open the management modal and press Run in chat on a workflow. Your next message drives the workflow and its progress is shown live in the assistant message; the message is labeled with the workflow name so you can see which one answered. While a workflow is the run-target, the per-message model / reasoning / verbosity / structured-output controls are hidden, because each step's model is fixed by the agent it invokes. The name shown in their place is also the way back: click it to reopen the Declarative Agents & Workflows modal.
    • As a background job -- run the same workflow as a pipeline job with run history and a captured log, for long or unattended runs.
  • Watch it as a diagram while it runs. Press Diagram on the run indicator to open a run canvas -- a movable, resizable window over the chat that shows the whole workflow graph lighting up step by step. See Watching a run below.
  • Pause for a human. Question and RequestExternalInput suspend the run and ask the operator -- an input form on the run canvas (interactive) or a "waiting for input" state in the Pipeline portal (background). Your answer resumes the same run, with its variables intact.
  • Author it as a diagram. Create -> New Workflow (and per-workflow Edit / Delete) opens a full-screen editor: a visual DAG canvas of your steps, a form for each step (an "Invoke agent" step picks one of your Prompt agents), and a live YAML preview you can also edit directly. Control-flow steps render as nested container nodes (see below), so branches and loops keep their shape on the canvas. Saving is a checkpoint: the editor stays open with your diagram and selection intact, the button reads Create the first time and Save afterwards, repeated saves update the same workflow, and Close is the only way out.
  • Mistakes are visible. An unsupported action, a reference to an agent that is not a Prompt agent, or a boundary-crossing action whose class is not enabled is flagged as a warning and blocks the workflow from running -- so a workflow that validates cleanly always runs.

The kind field

kindWhat it isHow you run it
PromptA single agentActivate it as the one active agent (a persona).
WorkflowAn orchestrationSelect it per-conversation as a run-target, or run it as a background job.

A workflow is not a persona: selecting it never changes your active agent, and it is never used by the OpenAI-compatible API or Teams (those always run the active Prompt agent). Workflows are web-app only.

The action surface

All 23 Microsoft Agent Framework declarative action kinds are authorable, grouped by category. The three boundary-crossing classes (Tool and HTTP) are off by default and enabled per-class in the environment -- see Boundary-crossing actions.

CategoryAction kinds
VariableSetVariable, SetMultipleVariables, SetTextVariable, ResetVariable, ClearAllVariables, ParseValue, EditTableV2
Control FlowIf, ConditionGroup, Foreach, BreakLoop, ContinueLoop, GotoAction
OutputSendActivity
AgentInvokeAzureAgent (invokes a declarative Prompt agent)
ToolInvokeFunctionTool, InvokeMcpTool -- jailed, opt-in
HTTPHttpRequestAction -- jailed, opt-in
Human-in-the-LoopQuestion, RequestExternalInput
Workflow ControlEndWorkflow, EndConversation, CreateConversation

Every action carries kind, id, and displayName

ElementRequiredPurpose
kindYesThe action type.
idYesUnique identifier. Also the target of a GotoAction. The editor always writes one.
displayNameNoA human label. It names the step in the run-progress indicator while the workflow runs.

Variables and namespaces

A variable path is always Namespace.name. A name with no namespace is stored as Local. for you -- on save, on read, and on compile -- so you can type counter and get Local.counter.

NamespaceContents
Local.*Read/write scratch variables inside the workflow.
Workflow.Inputs.*Values passed in when the workflow is started. Read-only.
Workflow.Outputs.*Values returned to the caller.
System.*Runtime-provided values such as the conversation id.

Every variable field in the editor suggests the variables this workflow already has -- the Local.* names used elsewhere in it, and the names declared in inputs: / outputs:. Fields that WRITE do not offer the read-only Workflow.Inputs.*. The list is a way to reuse a variable you already made, so a namespace on its own (Local.) is never suggested: it is a prefix, not a variable, and picking it would leave the field half-filled. The placeholder in each field shows the syntax instead. Suggestions carry exactly one namespace prefix however you typed the name -- a loop variable written as Local.item is offered as Local.item, never Local.Local.item, and a bare count is offered as Local.count.

Writing to a path the runtime cannot accept -- Workflow.Inputs.*, Workflow on its own, an unknown Workflow.<name>, or an empty path -- is a blocking warning, reported before the workflow runs rather than as an error in the middle of it.

A namespace the runtime does not recognize (for example topic.x) is a valid custom namespace and is left exactly as you wrote it. The editor simply does not suggest one.

Watching a run

Every workflow run shows a compact progress indicator inside the assistant message: the steps as they happen, a check when one completes, a distinct mark when one is skipped, and the error when one fails.

For anything more than a glance, press Diagram on that indicator to open the run canvas.

  • It opens itself. Sending a message to a workflow brings its canvas up; you do not have to press Diagram first. That button is there to bring a canvas back after you close it.
  • It is a window, not a panel. Drag its title bar to move it and its bottom-right corner to resize it, and drag the boundary between the two panes to give the graph or the step log more room. On a wide monitor you can keep the conversation and the diagram side by side.
  • Closing it is not losing it. The close button hides the window and keeps everything the run collected -- press Diagram again to bring the same canvas back, with its node states and logs intact.
  • One canvas per run. Starting another run opens another canvas instead of replacing the first, so you can compare a failing run against the one that worked. Beyond three visible at once the oldest is hidden (not discarded).
  • The whole graph, not just the path taken. The diagram is drawn from the workflow file before the first step reports, so branches that were not entered are still on screen. This matters: the runtime emits nothing at all for a branch it did not take, so a diagram assembled from the run alone could never show you the path that was skipped.
  • Per-step logs. Click a step to see what it produced, what it failed with, and which loop iteration it belonged to. Large payloads are truncated with an explicit marker.
  • Your steps, not the engine's. The Agent Framework runs helper executors of its own (an entry join, a condition evaluator for each If). They are not shown as steps: a condition evaluator lights up the If you wrote, and the step count matches what you see.
  • It does nothing to the run. The canvas is a viewer. Closing it has no effect on execution, and the in-message indicator keeps working either way.
  • Past runs come back. Reloading a chat restores each workflow turn's step list, its step logs, and its variables, and Diagram re-opens that run's canvas. Logs are trimmed when saved -- a long or loop-heavy run keeps its most recent entries -- so a restored run can show a little less than the live one did.
  • It opens itself when you are needed. A run that pauses for input, or that fails, brings its canvas up on its own, because you cannot answer a question from a window that is not there. Otherwise a run stays quiet until you press Diagram.

Watching the variables

The canvas has a Variables pane showing the Local. / Workflow.Inputs.* / Workflow.Outputs.* / System.* / Agent.* namespaces as the run progresses. No setup is required.

Workflow variables are visible to anyone who can open the canvas

Values whose name looks secret-like (api_key, client_secret, token, ...) are replaced with *** and long values are truncated -- but that is a name-based heuristic, not a guarantee. A secret you store in Local.x or Local.response will be displayed.

There is no setting that turns this off. If your deployment must not surface workflow variables, do not give those users access to the run canvas, and keep secrets out of workflow variables in the first place (the jailed HTTP / MCP / tool handlers hold their own credentials; a workflow never needs to carry one).

The pane is purely diagnostic -- reading the variables never changes what a workflow does.

Readings are taken at each superstep boundary, which is where the Agent Framework commits pending variable writes. A value therefore appears at the end of the step that set it, not mid-step.

Asking the user a question

Question pauses the workflow until the user answers. Each entry under choices is a value / label pair, and allowFreeText is always written out so the document states whether a typed answer is accepted:

- kind: Question
id: ask_priority
displayName: 優先度を質問
question:
text: 優先度を選択してください
variable: Local.priority
choices:
- value: high
label:
- value: low
label:
allowFreeText: false
default: medium

When the run reaches this step it pauses the turn rather than holding the connection open: the step is marked awaiting input, and the run canvas shows the prompt with your choices (plus a free-text box when allowFreeText is not false). Submitting the answer closes the form at once and continues the same run -- its Local. variables are still there, so the steps after the question see the value you gave. It also continues the same reply: output produced before the question and output produced after it end up in one message, separated by a blank line, just as they would in a workflow that never pauses. Because the turn ends rather than hanging, a question that is left unanswered survives a page reload.

Multi-worker deployments

A workflow waiting for an answer is held by the process that started it. If you run several backend workers behind a load balancer, use sticky routing for workflows that ask questions, or run them through the background Pipeline lane instead. If the paused run is no longer available (for example the server restarted), your answer is refused with an explanation rather than silently starting a new run against the wrong state.

Field names follow the runtime

Each action's fields are the ones the installed Microsoft Agent Framework runtime reads: SetTextVariable takes text, SetMultipleVariables takes an assignments list, ParseValue takes value (plus an optional valueType), and EditTableV2 takes item with an optional key / index. Workflows written with the older field names are migrated automatically the first time they are validated or run; the file itself is only rewritten when you save it.

Control flow is nested

Control flow is authored as nested container nodes in the DAG editor, matching the YAML tree exactly -- not as flat branch/merge edges:

  • If -- a container with a then lane and an else lane.

  • Foreach -- a container with one body lane; the header shows the source, item name, and index name. BreakLoop / ContinueLoop are authorable only inside a loop container. BreakLoop exits the enclosing loop; ContinueLoop skips to the next item.

    Fixed in 0.125.0

    Before 0.125.0, BreakLoop behaved exactly like ContinueLoop -- the loop ran to completion either way. A workflow authored against that behavior will now exit its loop early.

  • ConditionGroup -- one lane per condition (labeled), plus an elseActions lane.

  • GotoAction -- renders as a labeled edge to its target action (the one back-edge).

Nesting in the YAML defines scope, so the container nodes and the file round-trip without loss. You can always edit the raw YAML directly in the preview pane.

Boundary-crossing actions (opt-in)

Three action classes cross the credential / provider / network boundary the runtime otherwise keeps closed. They are off by default and enabled one class at a time in the environment. When a class is disabled, any use of its actions is a blocking warning with an actionable "enable the flag" message, so a workflow never silently reaches outside.

  • InvokeFunctionTool -- runs only the same function surface the invoked agent would have (coding still gated by CODING_ENABLED). Enabled by WORKFLOW_FUNCTION_ACTIONS_ENABLED.
  • InvokeMcpTool -- reaches only MCP servers/tools that are already configured and enabled in the gating store; it can never introduce a new server. Enabled by WORKFLOW_MCP_ACTIONS_ENABLED.
  • HttpRequestAction -- makes an outbound HTTP request, restricted to WORKFLOW_HTTP_ALLOWED_HOSTS (empty = deny all); an SSRF guard blocks loopback, private, link-local, and metadata addresses regardless of the allow-list. Enabled by WORKFLOW_HTTP_ACTIONS_ENABLED; WORKFLOW_HTTP_TIMEOUT_MS bounds each request.

Configuration

VariableDefaultPurpose
DECLARATIVE_AGENTS_DIR(unset)The folder workflows (and agents) are discovered from. Unset = no workflows. Authoring requires it to be writable.
WORKFLOW_MAX_ITERATIONS100A runaway-loop / cost guard capping a workflow's steps (the YAML maxTurns still applies as a smaller fallback).
WORKFLOW_FUNCTION_ACTIONS_ENABLEDfalseEnables the InvokeFunctionTool jailed handler (agent-equivalent function surface only).
WORKFLOW_MCP_ACTIONS_ENABLEDfalseEnables the InvokeMcpTool jailed handler (configured + gating-store-enabled MCP servers/tools only).
WORKFLOW_HTTP_ACTIONS_ENABLEDfalseEnables the HttpRequestAction jailed handler.
WORKFLOW_HTTP_ALLOWED_HOSTS(empty = deny all)Comma-separated host allow-list for HttpRequestAction. The SSRF guard still blocks loopback / private / link-local / metadata addresses.
WORKFLOW_HTTP_TIMEOUT_MS10000Per-request timeout for HttpRequestAction.

Example

A workflow declares optional inputs / outputs, a maxTurns cap, and a top-level actions list. Control-flow actions carry their child actions inline (then / else, actions, conditions).

name: workflow-name
description: workflow description
maxTurns: 100
inputs:
inputName:
type: string
description: input description
outputs:
outputName:
type: string
actions:
- kind: SetVariable
id: initialize
displayName: Read the input # optional; labels the step while it runs
variable: Local.value
value: =Workflow.Inputs.inputName
- kind: If
id: branch
condition: =Not(IsBlank(Local.value))
then:
- kind: InvokeAzureAgent
id: invoke_agent
agent:
name: MyAgent # resolves to a kind:Prompt declarative agent
input:
messages: =Local.value
output:
responseObject: Local.AgentResult
else:
- kind: SendActivity
id: invalid_input
activity:
text: Input is empty.
- kind: SetVariable
id: set_output
variable: Workflow.Outputs.outputName
value: =Local.AgentResult.summary
- kind: EndWorkflow
id: finish

Save it under DECLARATIVE_AGENTS_DIR, open the Declarative Agents & Workflows modal (the robot icon in the sidebar footer), select the workflow, and press Run in chat -- or run it as a background job.