Aleph
Architecture

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 featuresAgent / Clarify step kinds; review / timeout_seconds / max_retries per-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.js format (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 metadata timeout_secs (same override channel as task_create)
  • max_retries → task metadata max_retries
  • review: true → task metadata require_grounding / lead_review_required (read in agents::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 Strategyrender_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:

  1. Bare manifest JSON (starts with {) → exact parse, lossless
  2. Embedded block (/* @aleph-workflow {json} */) → exact parse, lossless
  3. Bare .workflow.js → lightweight scan of the pure-literal meta block + ordered phase() / agent() calls; imperative constructs go into dropped

Export (render_workflow_js) — Renders the manifest as a .workflow.js source string with:

  1. Lossless round-trip header (/* @aleph-workflow {json} */)
  2. meta block (pure literal)
  3. Body: topological layers → parallel() / sequential agent() 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 → active

This 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.json

See Also

On this page