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: WorkflowYAML into yourDECLARATIVE_AGENTS_DIR(the same folder as your agents) and ChatWalaʻau compiles it with the Microsoft Agent FrameworkWorkflowFactory. 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
InvokeAzureAgentsteps call yourkind: Promptagents (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.InvokeAzureAgentresolves 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/Workflowtag. 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 as a small graph; the assistant 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.
- As a background job -- run the same workflow as a pipeline job with run history and a captured log, for long or unattended runs.
- Pause for a human.
QuestionandRequestExternalInputsuspend the run and ask the operator -- an input card in chat (interactive) or a "waiting for input" state in the Pipeline portal (background). Your answer resumes the run where it parked. - 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.
- 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
kind | What it is | How you run it |
|---|---|---|
Prompt | A single agent | Activate it as the one active agent (a persona). |
Workflow | An orchestration | Select 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.
| Category | Action kinds |
|---|---|
| Variable | SetVariable, SetMultipleVariables, SetTextVariable, ResetVariable, ClearAllVariables, ParseValue, EditTableV2 |
| Control Flow | If, ConditionGroup, Foreach, BreakLoop, ContinueLoop, GotoAction |
| Output | SendActivity |
| Agent | InvokeAzureAgent (invokes a declarative Prompt agent) |
| Tool | InvokeFunctionTool, InvokeMcpTool -- jailed, opt-in |
| HTTP | HttpRequestAction -- jailed, opt-in |
| Human-in-the-Loop | Question, RequestExternalInput |
| Workflow Control | EndWorkflow, EndConversation, CreateConversation |
Every action carries kind, id, and displayName
| Element | Required | Purpose |
|---|---|---|
kind | Yes | The action type. |
id | Yes | Unique identifier. Also the target of a GotoAction. The editor always writes one. |
displayName | No | A 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.
| Namespace | Contents |
|---|---|
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 these namespaces, plus the Local.* names
already used elsewhere in the workflow and the names declared in inputs: / outputs:.
Fields that WRITE do not offer the read-only Workflow.Inputs.*.
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.
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
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 athenlane and anelselane.Foreach-- a container with one body lane; the header shows the source, item name, and index name.BreakLoop/ContinueLoopare authorable only inside a loop container.ConditionGroup-- one lane per condition (labeled), plus anelseActionslane.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 byCODING_ENABLED). Enabled byWORKFLOW_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 byWORKFLOW_MCP_ACTIONS_ENABLED.HttpRequestAction-- makes an outbound HTTP request, restricted toWORKFLOW_HTTP_ALLOWED_HOSTS(empty = deny all); an SSRF guard blocks loopback, private, link-local, and metadata addresses regardless of the allow-list. Enabled byWORKFLOW_HTTP_ACTIONS_ENABLED;WORKFLOW_HTTP_TIMEOUT_MSbounds each request.
Configuration
| Variable | Default | Purpose |
|---|---|---|
DECLARATIVE_AGENTS_DIR | (unset) | The folder workflows (and agents) are discovered from. Unset = no workflows. Authoring requires it to be writable. |
WORKFLOW_MAX_ITERATIONS | 100 | A runaway-loop / cost guard capping a workflow's steps (the YAML maxTurns still applies as a smaller fallback). |
WORKFLOW_FUNCTION_ACTIONS_ENABLED | false | Enables the InvokeFunctionTool jailed handler (agent-equivalent function surface only). |
WORKFLOW_MCP_ACTIONS_ENABLED | false | Enables the InvokeMcpTool jailed handler (configured + gating-store-enabled MCP servers/tools only). |
WORKFLOW_HTTP_ACTIONS_ENABLED | false | Enables 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_MS | 10000 | Per-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.