Workflow
Declarative, reusable workflow templates — the workflow half of the agent-workflow spectrum; compile to a coord_tasks DAG, executed by TeamDispatcher.
The workflow module provides declarative, reusable workflow templates that complement Aleph's agent-driven reasoning with predefined, deterministic orchestration. While agents dynamically direct their own process (Think→Act loop, LLM-authored team task DAG), workflows are named, saved, and re-runnable templates where LLMs are orchestrated through predefined code paths.
Overview
Workflows enable:
- Named, reusable templates — Save multi-step workflows to disk and run them on demand
- Declarative orchestration — Define agent steps, dependencies, and execution order as pure data
- Three step features —
Agent/Clarifystep kinds;review/timeout_seconds/max_retriesper-step overrides - DAG execution — Dependency edges drive Tokio-concurrent execution via
TeamDispatcher - Clarify interactivity — Pause the workflow to ask the user a structured question, then resume
- Claude Code interoperability — Bidirectional import/export of
.workflow.jsformat (via the AWI manifest superset) - MetaSkill proposals — Auto-drafted workflow templates from observed skill co-occurrence patterns
Design Philosophy
No new scheduler, no new reasoning.
A WorkflowDef is pure data. It is persisted to disk (store) and, on run, compiled (compile::materialize) into the coord_tasks DAG, then executed by the existing TeamDispatcher (src/teams/dispatcher/). This module contributes no scheduler and no reasoning — it is a schema + a file store + a deterministic compiler.
Each step is a full agent run; dependency edges drive concurrent execution that single-agent reference designs cannot express.
Architecture
src/workflow/
├── mod.rs # entry + public API; re-exports WorkflowDef / WorkflowManifest / materialize
├── def.rs # WorkflowDef / WorkflowStepDef / WorkflowStepKind + validate + topo_order
├── store.rs # filesystem persistence (atomic writes: temp-file + rename)
├── compile.rs # materialize a WorkflowDef into the coord_tasks DAG
├── clarify.rs # ClarifyContext / ClarifyTaskMeta / CLARIFY_META_KEY
├── proposal.rs # gated MetaSkill draft tier (proposals/ dir)
└── interop/ # bidirectional bridge with .workflow.js
├── mod.rs # parse_workflow_js / render_workflow_js / ImportOutcome
├── manifest.rs # WorkflowManifest — AWI (Aleph Workflow Interchange) superset
├── import.rs # three-path parse (bare JSON / embedded block / bare .workflow.js scan)
├── export.rs # render manifest as .workflow.js
└── consts.rs # bounded data-literal normalizer (bare-scan schema-const resolution)Core Components
WorkflowDef (src/workflow/def.rs)
pub struct WorkflowDef {
pub name: String, // storage key + coord_task subject prefix; sanitised on save
#[serde(default)]
pub description: String,
pub steps: Vec<WorkflowStepDef>, // execution order derives from depends_on, not list position
}
pub enum WorkflowStepKind {
#[default]
Agent, // run step.agent on step.prompt (default; legacy templates without `kind` deserialize unchanged)
Clarify, // pause the run, ask the user the prompt (optionally choices); runs no agent
}
pub struct WorkflowStepDef {
pub id: String, // step-local id; referenced by depends_on
#[serde(default)]
pub agent: String, // becomes coord_task.owner; ignored for Clarify
pub prompt: String, // {input} substituted; for Clarify this is the question
#[serde(default)]
pub depends_on: Vec<String>, // becomes coord_task.blocked_by
#[serde(default, skip_serializing_if = "WorkflowStepKind::is_agent")]
pub kind: WorkflowStepKind, // Agent (default) | Clarify
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub choices: Vec<String>, // Clarify menu; empty = free-text
#[serde(default, skip_serializing_if = "is_false")]
pub review: bool, // gate lead review: parks task in WaitingReview instead of Completed
#[serde(default, skip_serializing_if = "Option::is_none", alias = "timeout_secs")]
pub timeout_seconds: Option<u64>, // per-step timeout; None = global [team_dispatcher] task_timeout_secs
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_retries: Option<u32>, // per-step auto-retry ceiling; 0 = first failure is terminal
}WorkflowDef::validate checks: non-empty name, at least one step, unique step ids, every depends_on resolves to an existing step, no self-dependency, acyclic dependency graph (Kahn's topological sort proves it), and per-step rules: Agent requires both agent and prompt; Clarify requires a question (non-empty prompt) and forbids review / timeout_seconds / max_retries (there is no agent run to gate or time out).
A passing validate() guarantees topo_order() succeeds.
Materialisation (src/workflow/compile.rs)
The compiler maps each WorkflowStepDef to one coord_task owned by step.agent, with step.depends_on mapped to coord_task.blocked_by. Tasks are tagged {"managed_by": "dispatcher"} so the autonomous loop picks them up:
pub async fn materialize(
def: &WorkflowDef,
input: &str,
team_id: &str,
store: &dyn CoordTaskStore,
clarify_ctx: Option<&ClarifyContext>,
) -> Result<MaterializedWorkflow>;Tasks are created in topological order so each blocked_by references an already-minted task id. The rendered prompt substitutes {input} with the run input. Per-step overrides:
timeout_seconds→ task metadatatimeout_secs(same override channel astask_create)max_retries→ task metadatamax_retriesreview: true→ task metadatarequire_grounding/lead_review_required(read inagents::swarm::tasks::acceptance)
Upstream step outputs flow into each step automatically via the dispatcher's build_handoff_context. The run-global welded strategy frame (workflow_strategy) is stamped once per agent step from Strategy → render_workflow_global_frame.
Clarify Steps (src/workflow/clarify.rs)
A Clarify step pauses the DAG to collect a structured answer from the user. Its prompt is the question and choices (if any) the menu — agent is ignored. The step is owned by the sentinel __clarify__ and carries an awaiting record in the coord_task metadata:
pub struct ClarifyTaskMeta {
pub question: String,
pub choices: Vec<String>,
pub channel_id: String,
pub conversation_id: String,
pub session_key: String,
}
pub const CLARIFY_DELIVERY_PENDING_KEY: &str;
pub const CLARIFY_META_KEY: &str;
pub const CLARIFY_OWNER: &str = "__clarify__";The dispatcher detects clarify tasks via src/teams/dispatcher/clarify.rs::handle_clarify_task, delivers the question, and parks the task in Paused. The inbound router resolves the user's reply by completing the parked task with their answer. This design means a clarify step survives a process restart with no in-memory state to reconstruct.
Persistence (src/workflow/store.rs)
Workflows are stored as JSON files under $ALEPH_HOME/workflows/*.json. The persisted document is the WorkflowManifest superset — the single source of truth that preserves .workflow.js-compatible metadata (whenToUse, phases, per-step label/model/phase/schema/isolation/agentType) across import → save → export. Execution still consumes only the projected core via WorkflowManifest::to_def (R10 — the executor never sees the extra metadata).
pub fn workflow_dir() -> PathBuf; // $ALEPH_HOME/workflows/
pub struct WorkflowMeta { pub name: String }
pub fn resolve_path_at(dir: &Path, name: &str) -> PathBuf;
pub fn save_at(dir: &Path, manifest: &WorkflowManifest) -> Result<PathBuf>;
pub fn save(manifest: &WorkflowManifest) -> Result<PathBuf>;
pub fn write_text_at(dir: &Path, name: &str, ext: &str, body: &str) -> Result<PathBuf>;
pub fn load_at(dir: &Path, name: &str) -> Result<WorkflowManifest>;
pub fn load(name: &str) -> Result<WorkflowManifest>;
pub fn list_at(dir: &Path) -> Result<Vec<WorkflowMeta>>;
pub fn list() -> Result<Vec<WorkflowMeta>>;
pub fn delete_at(dir: &Path, name: &str) -> Result<bool>;
pub fn delete(name: &str) -> Result<bool>;Writes are atomic (temp-file + rename) so readers never see a torn write. resolve_path_at uses canvas_io::sanitise_name to prevent path traversal.
Claude Code Interoperability (src/workflow/interop/)
The interop module provides bidirectional translation between Aleph's WorkflowDef and Claude Code's .workflow.js engineering format:
Import (parse_workflow_js) — Three paths, in priority order:
- Bare manifest JSON (starts with
{) → exact parse, lossless - Embedded block (
/* @aleph-workflow {json} */) → exact parse, lossless - Bare
.workflow.js→ lightweight scan of the pure-literalmetablock + orderedphase()/agent()calls; imperative constructs go intodropped
Export (render_workflow_js) — Renders the manifest as a .workflow.js source string with:
- Lossless round-trip header (
/* @aleph-workflow {json} */) metablock (pure literal)- Body: topological layers →
parallel()/ sequentialagent()skeleton
Bare-scan schema-const resolution (R3-compliant): the engineering format hoists schema to a top-level const and references it by name; the bare scan resolves hoisted consts (and inline schemas) through a bounded data-literal normalizer in interop/consts.rs, normalizing JS-lax forms (bare keys / single quotes / trailing commas) to JSON. When it hits any expression value (identifier / function call / template string / computed key), the whole schema is deprecated and recorded as dropped.
MetaSkill Proposals (src/workflow/proposal.rs)
The proposal module implements a gated draft tier above the active workflow store. The dream pipeline mines recurring skill co-occurrence and drafts candidate workflows in proposals/. A proposal is never an active workflow — it only runs after an explicit accept (the "proposal gate"):
pub fn list_proposals() -> Result<Vec<WorkflowMeta>>;
pub fn accept(name: &str) -> Result<PathBuf>; // promote draft → activeThis keeps the loop R5-quiet — capabilities grow in the background, but nothing auto-activates and nothing steals focus.
Example Workflow
{
"name": "research-report",
"description": "Research a topic and write a report",
"steps": [
{
"id": "gather",
"agent": "researcher",
"prompt": "Research the topic: {input}. Collect key facts, sources, and perspectives."
},
{
"id": "ask-focus",
"prompt": "What angle should the report take?",
"kind": "clarify",
"choices": ["Technical deep-dive", "Executive summary", "Critical analysis"],
"depends_on": ["gather"]
},
{
"id": "write",
"agent": "writer",
"prompt": "Write a report on {input} based on the research and the chosen angle.",
"depends_on": ["ask-focus"],
"timeout_seconds": 900,
"max_retries": 1
},
{
"id": "review",
"agent": "editor",
"prompt": "Review the report for clarity, accuracy, and completeness. Suggest improvements.",
"depends_on": ["write"],
"review": true
}
]
}This defines a diamond-like workflow: research → clarify → write → review. The Clarify step pauses execution to ask the user for focus direction; review: true lets the leader decide completion.
Execution Flow
User runs workflow(action='run', name='research-report', input='quantum computing')
│
▼
┌─────────────────────────────────────────────────────┐
│ Gateway: workflow tool handler │
│ • Load WorkflowManifest from store │
│ • WorkflowManifest::to_def → WorkflowDef │
│ • WorkflowDef::validate │
│ • Ensure team exists │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Workflow compiler (compile::materialize) │
│ • Topological sort steps (Kahn) │
│ • Create coord_task for each step │
│ • Map depends_on → blocked_by edges │
│ • Substitute {input}; stamp strategy / model / effort metadata │
│ • Tag tasks as dispatcher-managed │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ TeamDispatcher (src/teams/dispatcher/) │
│ • Pick up pending tasks │
│ • For agent steps: route to step.agent │
│ • For clarify steps: CLARIFY_OWNER parks task │
│ • Build handoff context from upstream outputs │
│ • Execute with Tokio concurrency │
└─────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────┐
│ Inbound Router (on user reply to clarify) │
│ • Resolve session from reply │
│ • Complete parked clarify task │
│ • Unblock downstream steps │
└─────────────────────────────────────────────────────┘Configuration
Workflows are stored in $ALEPH_HOME/workflows/ (falls back to ~/.aleph/workflows/, then ./workflows/):
~/.aleph/
├── workflows/
│ ├── research-report.json
│ ├── deploy-pipeline.json
│ └── proposals/
│ ├── metaskill-git-pr.json
│ └── metaskill-research-write.jsonSee Also
- Teams — Team coordination and
TeamDispatcher - Orchestrator — AgentDef + FlowSpec resolution
- Harness — Think→Act loop that executes each step
- Dispatcher — Task orchestration and tool filtering
- Tools — Tool execution architecture
- Workflow Interop Doc — full protocol
- Teams Architecture
- MCP Integration