Building Workflows
Step-by-step guide to writing, deploying, and running multi-agent workflow FSMs with the Forge reference pattern.
Building Workflows
This guide walks through creating a multi-agent workflow using the AEGIS workflow YAML format. You will build a simplified version of the Forge reference pattern: a pipeline that takes a programming task through requirements analysis, implementation, and review.
Prerequisites
- AEGIS daemon running with Temporal configured (see Getting Started)
- At least three agents deployed:
requirements-agent,coder-agent,reviewer-agent - Basic familiarity with Workflows
Before You Start: Check for Existing Workflows
If your node has the discovery service configured (enterprise feature), search for existing workflows that might already cover your use case before writing a new one:
// Tool Call: aegis.workflow.search
{ "query": "code review pipeline with human approval" }This avoids creating duplicate workflows and helps you build on proven patterns. On nodes without discovery, use aegis workflow list to browse the registry manually.
Workflow Metadata: description
Every workflow manifest should include metadata.description.
The description field is a human-readable summary of what the workflow accomplishes. It is the
primary input to the semantic embedding used by the discovery service — write it to capture intent
and capability, not implementation steps.
metadata:
name: my-pipeline
version: "1.0.0"
description: "Builds, tests, and deploys a Node.js service to Kubernetes on every merge."
labels:
category: deployment
team: platformDeclaring structured inputs with input_schema
If your workflow accepts named runtime inputs, declare them in metadata.input_schema. This makes the contract between callers and the workflow explicit, and enables the orchestrator to validate input payloads before the workflow starts.
metadata:
name: my-pipeline
version: "1.0.0"
input_schema:
type: object
properties:
task:
type: string
description: Natural language description of the task to complete
language:
type: string
enum: [python, typescript, rust]
required:
- taskInput fields are then available in state templates as {{input.task}}, {{input.language}}, etc. See the Workflow Manifest Reference for the full field specification.
Step 1: Write the Workflow Manifest
Create my-pipeline.yaml:
apiVersion: 100monkeys.ai/v1
kind: Workflow
metadata:
name: dev-pipeline
version: "1.0.0"
description: "Requirements analysis, human approval, implementation, and code review pipeline."
labels:
category: development
team: platform
tags:
- code-review
- ci-cd
- human-in-the-loop
- software-development
spec:
context:
language: python
test_framework: pytest
initial_state: REQUIREMENTS
states:
# Stage 1: Analyze the task and produce a requirements document
REQUIREMENTS:
kind: Agent
agent: requirements-agent
timeout: 180s
transitions:
- condition: on_success
target: APPROVE_REQUIREMENTS
- condition: on_failure
target: FAILED
# Stage 2: Human approval gate before implementation begins
APPROVE_REQUIREMENTS:
kind: Human
prompt: |
Requirements:
{{REQUIREMENTS.output}}
Approve? (yes/no)
timeout: 86400s # Wait up to 24 hours for operator approval
transitions:
- condition: input_equals_yes
target: IMPLEMENT
- condition: input_equals_no
target: FAILED
feedback: "{{human.feedback}}"
# Stage 3: Implement based on the approved requirements
IMPLEMENT:
kind: Agent
agent: coder-agent
timeout: 600s
input: |
Implement in {{workflow.context.language}} using {{workflow.context.test_framework}}.
Requirements: {{REQUIREMENTS.output}}
transitions:
- condition: on_success
target: REVIEW
- condition: on_failure
target: FAILED
# Stage 4: Review the implementation
REVIEW:
kind: Agent
agent: reviewer-agent
timeout: 300s
transitions:
- condition: score_above
threshold: 0.85
target: DONE
- condition: score_below
threshold: 0.85
target: FAILED
feedback: "Review score too low ({{REVIEW.score}}): {{REVIEW.output.reasoning}}"
DONE:
kind: System
command: "echo 'Pipeline complete. Output at /workspace/output.'"
transitions: []
FAILED:
kind: System
command: "echo 'Pipeline failed'"
transitions: []Step 2: Using Blackboard Templates
Agents in downstream states automatically receive earlier states' outputs via Handlebars templates in the input field. For example, the coder-agent's input already references {{REQUIREMENTS.output}} from Step 1.
At execution time, the rendered input is available via task.input in the runtime:
from aegis import AegisClient
client = AegisClient()
task = client.get_task()
# The rendered input is available directly
print(task.input) # "Implement in python using pytest.\nRequirements: ..."
# You can also access the raw blackboard if needed
language = task.blackboard.get("language", "python")For the full list of available template variables, see the Workflow Manifest Reference — Handlebars Template Variables.
Step 3: Deploy the Workflow
aegis workflow deploy ./my-pipeline.yaml --forceUse --force when you are re-deploying the same workflow name and version pair.
Expected output:
Deployed workflow "dev-pipeline" (id: wf-a1b2c3d4-0000-...)List deployed workflows:
aegis workflow listStep 4: Start a Workflow Execution
aegis workflow run dev-pipeline \
--input '{"task": "Write a REST API for a simple to-do list with CRUD operations."}'Expected output:
Workflow execution started: wfx-a1b2c3d4-1111-...Watch execution logs:
aegis workflow logs wfx-a1b2c3d4-1111-... --follow --verboseUse aegis workflow executions get wfx-a1b2c3d4-1111-... when you want a point-in-time view of
the execution status, blackboard, and persisted metadata.
Example output:
Execution ID: wfx-a1b2c3d4-1111-...
Workflow: dev-pipeline
Status: running
Current State: APPROVE_REQUIREMENTS (WAITING FOR HUMAN SIGNAL)
State History:
REQUIREMENTS completed (2026-02-23T10:00:01Z → 10:02:14Z)
APPROVE_REQUIREMENTS waiting (2026-02-23T10:02:15Z → ...)Step 5: Signal a Human State
The workflow is suspended at APPROVE_REQUIREMENTS. Resume it through the workflow CLI:
aegis workflow signal wfx-a1b2c3d4-1111-... --response approvedTo reject with feedback:
aegis workflow signal wfx-a1b2c3d4-1111-... \
--response "rejected: Requirements need more detail on error handling"After approval, the workflow continues to the IMPLEMENT state automatically.
Step 6: Monitor to Completion
# Continue watching logs
aegis workflow logs wfx-a1b2c3d4-1111-... --follow --verboseUsing ParallelAgents States
Run multiple judges concurrently and gate on consensus. Add a AUDIT state that evaluates the implementation from multiple perspectives simultaneously:
AUDIT:
kind: ParallelAgents
agents:
- agent: security-reviewer
input: "Security audit: {{IMPLEMENT.output}}"
weight: 2.0 # weighted higher
timeout_seconds: 300
- agent: performance-reviewer
input: "Performance review: {{IMPLEMENT.output}}"
weight: 1.0
timeout_seconds: 180
- agent: style-reviewer
input: "Style review: {{IMPLEMENT.output}}"
weight: 1.0
timeout_seconds: 60
consensus:
strategy: weighted_average # weighted_average | majority | unanimous | best_of_n
threshold: 0.85
min_agreement_confidence: 0.75
min_judges_required: 2 # at least 2 of 3 must succeed
timeout: 600s
transitions:
- condition: consensus
threshold: 0.85
agreement: 0.75
target: DONE
- condition: score_below
threshold: 0.85
target: FAILED
feedback: |
Audit failed (score: {{AUDIT.consensus.score}}, confidence: {{AUDIT.consensus.confidence}}):
Security: {{AUDIT.agents.0.output}}
Performance: {{AUDIT.agents.1.output}}
Style: {{AUDIT.agents.2.output}}All three judges run simultaneously. The state waits until all have completed (or timeout is hit), then evaluates the consensus block. See the Workflow Manifest Reference — kind: ParallelAgents for all consensus strategies.
Composing Workflows with Subworkflows
As your workflow library grows, you will find common patterns repeated across pipelines — data processing, deployment sequences, validation gates. The Subworkflow state kind lets you extract these into standalone workflows and invoke them from a parent.
Example: Parent Workflow Calling a Reusable Validation Pipeline
Suppose you have a reusable validation-pipeline workflow that runs security scanning, linting, and test execution. You can invoke it from any parent workflow:
Child workflow (validation-pipeline.yaml, deployed separately):
apiVersion: 100monkeys.ai/v1
kind: Workflow
metadata:
name: validation-pipeline
version: "1.0.0"
spec:
initial_state: SECURITY_SCAN
states:
SECURITY_SCAN:
kind: Agent
agent: security-scanner
intent: "{{intent}}"
input: |
Repository: {{input.repo_url}}
Branch: {{input.branch}}
timeout: 300s
transitions:
- condition: on_success
target: LINT
- condition: on_failure
target: FAILED
LINT:
kind: ContainerRun
image: "rust:1.75-alpine"
command: ["cargo", "clippy", "--all-targets", "--", "-D", "warnings"]
workdir: "/workspace"
volumes:
- name: workspace
mount_path: /workspace
transitions:
- condition: exit_code_zero
target: TEST
- condition: exit_code_non_zero
target: FAILED
TEST:
kind: ContainerRun
image: "rust:1.75-alpine"
command: ["cargo", "test", "--workspace"]
workdir: "/workspace"
volumes:
- name: workspace
mount_path: /workspace
transitions:
- condition: exit_code_zero
target: DONE
- condition: exit_code_non_zero
target: FAILED
DONE:
kind: System
command: "echo 'Validation passed'"
transitions: []
FAILED:
kind: System
command: "echo 'Validation failed'"
transitions: []Agent states have two distinct template fields: intent: carries the natural-language steering ("why") and input: carries the structured data ("what"). They are separate concerns — intent: resolves to {{intent}} inside the agent's prompt template, while input: resolves to {{input}}. When intent: is omitted on a state, {{intent}} in the agent's prompt falls back to the workflow-level caller intent.
Parent workflow that invokes the child:
apiVersion: 100monkeys.ai/v1
kind: Workflow
metadata:
name: release-pipeline
version: "1.0.0"
spec:
initial_state: GENERATE_CODE
states:
GENERATE_CODE:
kind: Agent
agent: coder-v2
input: "{{intent}}"
timeout: 600s
transitions:
- condition: on_success
target: VALIDATE
- condition: on_failure
target: FAILED
VALIDATE:
kind: Subworkflow
workflow_id: validation-pipeline
mode: blocking
result_key: validation_result
input: "Validate code generated for: {{intent}}"
timeout: 1800s
transitions:
- condition: on_success
target: DEPLOY
- condition: on_failure
target: FIX_CODE
feedback: "Validation failed: {{VALIDATE.result}}"
FIX_CODE:
kind: Agent
agent: coder-v2
input: |
Fix the code based on validation feedback:
{{state.feedback}}
timeout: 600s
transitions:
- condition: on_success
target: VALIDATE
- condition: on_failure
target: FAILED
DEPLOY:
kind: ContainerRun
image: "docker:24-cli"
command: ["sh", "-c", "echo 'Deploying validated artifact'"]
transitions:
- condition: exit_code_zero
target: DONE
- condition: exit_code_non_zero
target: FAILED
DONE:
kind: System
command: "echo 'Release complete'"
transitions: []
FAILED:
kind: System
command: "echo 'Release failed'"
transitions: []The VALIDATE state starts the validation-pipeline child workflow in blocking mode. The parent waits for the child to complete and then stores the child's final Blackboard under the validation_result key. If the child fails, the parent routes to FIX_CODE with the child's output as feedback.
Fire-and-Forget for Background Tasks
Use fire_and_forget mode when you want to trigger a workflow without waiting for it to finish — for example, kicking off a notification pipeline or a cleanup job:
NOTIFY:
kind: Subworkflow
workflow_id: notification-pipeline
mode: fire_and_forget
input: |
{"message": "Deployment complete", "channel": "ops"}
transitions:
- condition: on_success
target: DONEThe parent continues immediately. The child's execution ID is available as {{NOTIFY.child_execution_id}} for later inspection.
For the complete field specification and validation rules, see the Workflow Manifest Reference — kind: Subworkflow.
Retry Loops and Visit Limits
When a workflow contains backward transitions -- for example, a validation state that routes back to a code-generation state on failure -- the FSM can enter an unbounded retry loop if the upstream state never produces acceptable output. Two fields prevent this:
max_state_visits(per state, default5, ceiling20): limits how many times the FSM can enter a given state. Once exceeded, the workflow terminates.max_total_transitions(workflow level, default50, ceiling100): limits the total number of state-to-state transitions across the entire execution.
Set these values based on the expected retry depth of your workflow. Lower values are appropriate for pipelines where retries are unlikely to succeed after a few attempts; higher values suit workflows with legitimate multi-pass refinement.
spec:
initial_state: GENERATE
max_total_transitions: 30
states:
GENERATE:
kind: Agent
agent: coder-v1
max_state_visits: 5
input: |
{{#if state.feedback}}
Previous attempt failed: {{state.feedback}}
Fix the code.
{{else}}
{{intent}}
{{/if}}
transitions:
- condition: always
target: VALIDATE
VALIDATE:
kind: Agent
agent: reviewer-v1
max_state_visits: 5
transitions:
- condition: score_above
threshold: 0.85
target: DONE
- condition: score_below
threshold: 0.85
target: GENERATE
feedback: "{{VALIDATE.output.reasoning}}"
DONE:
kind: System
command: "echo 'Complete'"
transitions: []In this example, VALIDATE can route back to GENERATE up to 5 times. If either state hits its max_state_visits limit or the workflow exceeds 30 total transitions, execution terminates. These limits are independent of max_iterations, which controls the agent's inner refinement loop within a single state execution.
Temporal UI
Temporal provides a built-in workflow visibility UI that shows the durable state of every workflow execution. Access it at http://localhost:8233 when running the local dev stack.
The UI shows:
- Current state of each execution
- History of state transitions
- Any pending timers (e.g., Human state timeouts)
- Detailed error information for failed executions
This is the authoritative view for debugging stuck or failed workflow executions.
Workflow Scope
Every workflow has a scope field in its metadata that controls which callers can discover and
invoke it:
| Scope | Who Can Use It |
|---|---|
tenant | All members of your organization (or the individual consumer user's per-user tenant) |
global | All tenants — reserved for platform builtins |
Newly registered workflows default to tenant scope. You do not need to specify scope explicitly
in the manifest.
Global scope is reserved for platform builtins such as builtin-intent-to-execution. Promoting
a workflow's scope to global is an admin operation.
When referencing a workflow by name — for example in aegis workflow run <name> or an
aegis.workflow.run tool call — the system resolves using narrowest-first order: tenant-scoped
workflows first, then global builtins. A tenant-scoped workflow named my-pipeline will be found
before a global workflow of the same name.
Workflow Manifest Reference
For the complete field specification including all StateKind options, TransitionRule operators, and Blackboard template variables, see the Workflow Manifest Reference.
For current CLI vs API workflow operation coverage, see the CLI Capability Matrix.