Workflows

Workflows define triggers, manual inputs, and lifecycle stages, then run inside a published workspace.

How workflows work

A workflow watches an external system or accepts a manual trigger, creates an agent with scoped access from its workspace, injects event context, and tracks the work through issue, code, PR, review, and completion states.

Author workflow YAML under .elasticclaw/workflows/, then publish it to a workspace with elasticclaw workflow push.

Deterministic steps and capabilities

Not every stage step needs the agent to decide. ElasticClaw Server can advance the workflow graph, run commands, inspect structured output, and call issue/PR APIs without interpreting chat. Prefer these hub-owned steps whenever the condition is known and testable.

Full stage field reference lives on the Stages page. This section is a capability map of what is deterministic today.

Hub-owned stage triggers

These conditions transition into a stage without asking the model to restate what happened:

pr_merged — Tracked pull request merges.

pr_closed — Tracked pull request closes without merge.

pr_conditions — Compound PR state. Supports ci: passing (checks success or skipped), reviews: clean (no changes-requested), and optional quiet_for (e.g. 30m, 1h) with no new comments in that window.

gate_result — Earlier stage gate verdict is pass or fail.

output_matches — Named pipeline output at a JSON path matches any value in any_of.

judge_verdict — Most recent judge stage reported pass or fail. The transition is deterministic; the verdict itself came from a model review.

yaml
stages:
  - id: ready_to_merge
    label: Ready to merge
    triggers:
      - pr_conditions:
          ci: passing
          reviews: clean
          quiet_for: 30m
    on_enter:
      inject: |
        CI is green and reviews are clean. Merge when ready.

  - id: merged
    label: Merged
    terminal: true
    triggers:
      - pr_merged: {}
    on_enter:
      add_labels: [done]
      remove_labels: [needs-review]

  - id: closed_no_merge
    label: Closed without merge
    terminal: true
    triggers:
      - pr_closed: {}

Deterministic gates over command output

After on_enter, a stage may evaluate a gate against JSON produced by a run (or other action that persists output). Gates do not ask a model to reinterpret logs.

run — Shell command in the sandbox; optional output stores parsed JSON for later stages and templates ({{ .Outputs.name.field }}).

gate — Pass/fail over a named output path with pass / fail value lists, required, and treat_skipped_as_pass.

gate_result — Branch on that verdict from another stage.

yaml
stages:
  - id: validation
    label: Validation
    on_enter:
      run:
        command: python3 scripts/validate.py
        output: validation
        timeout: 15m
    gate:
      output: validation
      pass:
        path: status
        values: [clean]
      fail:
        path: status
        values: [issues, error]
      required: true

  - id: create_pr
    triggers:
      - gate_result:
          stage: validation
          verdict: pass
    on_enter:
      inject: |
        Validation status: {{ .Outputs.validation.status }}. Open the PR.

  - id: fix_validation
    triggers:
      - gate_result:
          stage: validation
          verdict: fail
    on_enter:
      inject: |
        Validation failed: {{ .Outputs.validation.reason }}
        Fix, re-run checks, then continue.

Plan approval: freeform default vs deterministic plan_gate

Before implementation, ElasticClaw can require a visible plan. There are two modes — pick one path per workflow so plans are never double-approved:

Default (freeform) — If the workflow has no plan_gate: true stage, issue and workflow agents get a hub prompt to write a plan in chat. The hub accepts a substantial assistant message, then injects proceed. Existing installs keep this behavior without YAML changes.

Deterministic (plan_gate) — Opt in by setting plan_gate: true on a stage that also has a gate: block. Freeform plan approval is skipped entirely for that pipeline. Plan acceptance is a normal gate over structured JSON (for example a plan.json schema check), then gate_result advances to implementation.

Ordinary validation gates (tests, scanners, CodeBuild) without plan_gate: true do not disable freeform plan approval. Only an explicit plan gate opts out.

Recommended pattern: agent writes a plan artifact, emits a signal token, hub runs a schema validator, gate pass injects proceed.

yaml
stages:
  - id: plan
    label: Plan
    entry: true
    on_enter:
      move_issue: In Progress
      inject: |
        Issue: {{.Issue.Identifier}} — {{.Issue.Title}}
        URL: {{.Issue.URL}}

        Before implementing, write .elasticclaw/plan.json with:
          understanding  (string)
          area           (string)
          steps          (array of strings)
          verification   (string)

        Then say exactly: [PLAN_READY]
        Do not edit product code until the next stage injects proceed.

  - id: plan_validate
    label: Validate plan
    plan_gate: true          # opts out of freeform hub plan approval
    triggers:
      - message_contains: "[PLAN_READY]"
    on_enter:
      run:
        # Schema-only check — no NLP. Print JSON: {"status":"ok"|"incomplete",...}
        command: |
          python3 - <<'PY'
          import json, pathlib, sys
          p = pathlib.Path(".elasticclaw/plan.json")
          try:
              data = json.loads(p.read_text())
          except Exception as e:
              print(json.dumps({"status": "incomplete", "reason": f"unreadable: {e}"}))
              sys.exit(0)
          if not isinstance(data, dict):
              print(json.dumps({
                  "status": "incomplete",
                  "reason": "plan.json must contain a JSON object",
              }))
              sys.exit(0)
          # Require non-empty strings (not just truthy values like true/1).
          string_fields = ("understanding", "area", "verification")
          invalid = [
              k for k in string_fields
              if not isinstance(data.get(k), str) or not data[k].strip()
          ]
          steps = data.get("steps")
          if invalid:
              print(json.dumps({"status": "incomplete", "reason": f"invalid {invalid}"}))
          elif not isinstance(steps, list) or not steps or any(
              not isinstance(step, str) or not step.strip() for step in steps
          ):
              print(json.dumps({
                  "status": "incomplete",
                  "reason": "steps must be a non-empty list of strings",
              }))
          else:
              print(json.dumps({"status": "ok"}))
          PY
        output: plan
        timeout: 2m
    gate:
      output: plan
      pass:
        path: status
        values: [ok]
      fail:
        path: status
        values: [incomplete]
      required: true

  - id: implement
    label: Implement
    triggers:
      - gate_result:
          stage: plan_validate
          verdict: pass
    on_enter:
      inject: |
        Plan accepted (status {{ .Outputs.plan.status }}).
        Implement now. When the PR is ready, say [DONE] with the PR URL.

  - id: fix_plan
    label: Fix plan
    triggers:
      - gate_result:
          stage: plan_validate
          verdict: fail
    on_enter:
      inject: |
        Plan incomplete: {{ .Outputs.plan.reason }}
        Update .elasticclaw/plan.json, then say [PLAN_READY] again.

On plan_gate pass, the hub marks the plan accepted so freeform approval cannot re-fire if something races. Proceed is the implement stage's inject — not a keyword match on chat prose.

Full stage field notes (including plan_gate) are on the Stages page.

Hub-owned on-enter actions

These run on the server when a stage is entered (no chat marker required):

run — Deterministic command execution (command, timeout, continue_on_error, output).

dependency_updates — Ecosystem dependency bumps with structured JSON output. See Dependency updates.

add_labels / remove_labels — GitHub issue labels.

move_issue — Linear, Jira, Shortcut, or GitHub issue status (string or { status, issue_id }).

close_issue — Close the associated GitHub issue.

merge_pr — Request merge of the tracked PR through the server GitHub path.

notify — Send a message via a hub-configured notifier (for example Slack).

Skip rules

skip_if and skip_unless jump to another stage before on_enter when issue labels match (or do not match). Evaluation is deterministic over tracker labels only—not pipeline outputs.

yaml
stages:
  - id: working
    entry: true
    skip_if:
      issue_labels:
        labels: [skip-agent]
      go_to: skipped
    on_enter:
      inject: Read CONTEXT.md and start working.

  - id: skipped
    terminal: true

Not deterministic (agent or model)

message_contains — Matches agent chat text such as [DONE]. Convenient, but prose is not trusted evidence of a PR, CI result, or side effect.

inject — Sends instructions to the agent; useful, but not a proof that work completed.

judge — Model-backed review with bounded inputs. Use for subjective quality; use gate for tool JSON and pr_conditions for CI/review state.

Prefer hub-owned triggers and gates when the next step must be reliable: green CI, clean reviews, merge/close, command exit JSON, or label policy. Keep chat markers for agent UX, not as the only proof that an external system changed.

Workflow file

yaml
schema_version: v1
name: triage
enabled: true

trigger:
  github_issues:
    event: issue_labeled
    repositories:
      - my-org/my-app
    states:
      - open
    labels:
      - agent-ready
    exclude_labels:
      - do-not-automate
    labelers:
      - "*"

provider: daytona
tags: ["triage"]
color: teal

secret_refs:
  GITHUB_TOKEN: github_app

enable_manual_trigger: true
inputs:
  - name: issue
    type: string
    required: true

stages:
  - id: working
    label: Working
    entry: true
    on_enter:
      remove_labels: [agent-ready]
      add_labels: [agent-working]
      inject: |
        Issue: {{.Issue.Identifier}} — {{.Issue.Title}}
        URL: {{.Issue.URL}}

        Read CONTEXT.md and start working.

  - id: pr_opened
    label: PR Opened
    triggers:
      - message_contains: "[DONE]"
    on_enter:
      add_labels: [needs-review]
      remove_labels: [agent-working]

  - id: merged
    label: Merged
    triggers:
      - pr_merged: {}
    terminal: true

Workflow fields

name — Workflow identifier inside the workspace.

enabled — Set false to pause the workflow.

trigger.github_issues — GitHub Issues source. Supports issue events, repositories, states, required labels, excluded labels, labelers, and assignee filters.

trigger.linear — Linear source. Supports status-change events, states, team, projects, required labels, excluded labels, and assignee filters.

trigger.jira — Jira source. Supports status-change events, project keys, states, required labels, excluded labels, and assignee filters.

trigger.shortcut — Shortcut source. Supports status-change events, workspace, states, required labels, excluded labels, and assignee filters.

provider — Sandbox provider override for agents created by this workflow.

tags and color — Dashboard metadata for created agents.

secret_refs — Environment variable to workspace secret name map.

inputs — Manual trigger inputs.

concurrency_group — Limit parallel agents by group.

working_status — Move the source issue to this status when the agent starts.

enable_manual_trigger — Allow dashboard and CLI manual triggers.

analytics_enabled — Enable run analytics for this workflow (defaults to true for new workflows).

stages — Lifecycle stages used by the workflow.

Issue trigger label filters

Issue-tracker triggers can require labels and reject labels at the same time. All configured labels must be present, and no configured exclude_labels may be present. Matching is case-insensitive and ignores leading or trailing whitespace.

yaml
trigger:
  github_issues:
    event: issue_labeled
    repositories:
      - my-org/my-app
    labels:
      - agent-ready
    exclude_labels:
      - do-not-automate
      - blocked

Use exclude_labels for labels such as blocked, security-hold, or needs-human-review that should prevent automatic agent creation even when the normal trigger labels are present.

Linear and Jira project filters

Linear and Jira triggers can restrict creation to specific projects. Use project IDs or names for Linear; use project keys for Jira.

yaml
trigger:
  linear:
    event: status_changed
    team: ADV
    projects:
      - Adversary Labs
      - 123e4567-e89b-12d3-a456-426614174000
    states:
      - Todo

  jira:
    event: status_changed
    projects:
      - ENG
      - OPS
    states:
      - "To Do"

Run commands and gates

Workflow stages can run deterministic commands in the agent workspace, persist structured output, and use gates to choose the next stage. This is useful for tests, security scanners, deploy previews, CodeBuild jobs, or any tool that can print JSON.

yaml
stages:
  - id: validation
    label: Validation
    triggers:
      - message_contains: "[DONE]"
    on_enter:
      run:
        command: python3 scripts/validate.py
        output: validation
        timeout: 30m
        continue_on_error: false
    gate:
      output: validation
      pass:
        path: status
        values:
          - clean
      fail:
        path: status
        values:
          - issues
          - error
      required: true
      treat_skipped_as_pass: true

  - id: create_pr
    label: Create PR
    triggers:
      - gate_result:
          stage: validation
          verdict: pass
    on_enter:
      inject: |
        Validation status: {{ .Outputs.validation.status }}.
        Create the PR now.

  - id: fix_validation
    label: Fix Validation
    triggers:
      - gate_result:
          stage: validation
          verdict: fail
    on_enter:
      inject: |
        Validation failed: {{ .Outputs.validation.reason }}
        Fix the issue, commit locally, then say [DONE].
Commands should print a JSON object to stdout. ElasticClaw also accepts noisy stdout when the final line is JSON, such as shell trace output followed by {"status":"clean"}. treat_skipped_as_pass is for missing or skipped outputs that should continue through gate_result: pass.

Failure feedback

If a workflow-created agent stops because provisioning, bootstrap, workspace setup, a workflow command, a required gate, or an issue action fails, ElasticClaw marks the run failed and posts a sanitized issue-tracker comment when the workflow has issue context. The comment summarizes what failed and suggests the next diagnostic step without dumping raw logs or secrets.

Failure comments are best-effort. If the tracker token cannot comment, the dashboard and run status still show the failure.

Review stages

A judge stage runs a model-backed review over bounded inputs such as the issue, current diff, captured test output, or selected files. Use judge stages for subjective review, and gates for deterministic tool results.

yaml
stages:
  - id: review
    label: Review
    triggers:
      - message_contains: "[READY_FOR_REVIEW]"
    on_enter:
      judge:
        model: anthropic/claude-sonnet-4-6
        inputs:
          - issue
          - git_diff
          - test_output
        output: review_result
        instructions: |
          Decide whether the implementation satisfies the issue.
        require:
          verdict: pass

  - id: fix_review
    triggers:
      - judge_verdict: fail
    on_enter:
      inject: |
        Review failed. Apply the requested fixes and say [READY_FOR_REVIEW].
judge_verdict matches the most recent judge verdict for the workflow. Keep judge branches unambiguous; use deterministic gates when a transition must be scoped to a specific tool stage.

Skip rules

Stages can skip based on issue labels before the stage is entered. Use skip_if to jump to another stage when the issue has any of the listed labels, or skip_unless to jump when the issue has none of them. The target stage is set with go_to.

yaml
stages:
  - id: working
    label: Working
    entry: true
    skip_if:
      issue_labels:
        labels:
          - skip-agent
      go_to: skipped
    on_enter:
      inject: Read CONTEXT.md and start working.

  - id: skipped
    label: Skipped
    terminal: true

Run history and logs

Every workflow trigger (cron, manual, or issue tracker) creates a run record. Use the CLI to list recent runs and inspect agent logs for a specific run.

bash
elasticclaw workflow runs triage --workspace my-app --limit 20
elasticclaw workflow logs triage 5f35f8f6-7a2a-4f32-bb50-6e0cbd53c6ef --workspace my-app

Run records include status, trigger type, timestamps, and the linked agent ID. The dashboard also shows run history and agent activity logs.

Workflow v2 schema

schema_version: 2 workflows replace the transcript-driven v1 stage model with a deterministic state machine. The workflow declares states, transitions, commands, CI/review policies, delivery constraints, and cron triggers. Authority comes from the paired workspace v2, so the workflow no longer embeds repositories, credentials, or provider settings.

v1 workflows remain fully supported. v2 is opt-in via schema_version: 2.

Example v2 workflow

yaml
schema_version: 2
name: pull-request-delivery
enabled: true
initial_state: implementing

states:
  implementing:
    description: Implement the change and open a verified pull request.
    phase: build
    on_enter:
      effects:
        - agent.task:
            prompt: |
              Implement the requested change in the workspace, commit the
              result, and open a pull request. The workflow advances when the
              source-control connection reports the verified PR.
  awaiting_ci:
    description: A verified pull request exists and CI is unresolved.
    phase: pr
    invariant:
      pull_request:
        state: open
  fixing:
    description: Address verified CI or review failures and push fixes.
    phase: build
    on_enter:
      effects:
        - agent.task:
            prompt: |
              Inspect the CI or review failures for the current PR, apply
              fixes, commit, and push to the PR branch. The workflow advances
              when the PR head changes.
  awaiting_review:
    description: CI policy is satisfied.
    phase: review
  ready_to_merge:
    description: Ready.
    phase: review
  completed:
    phase: done
    terminal: true
  cancelled:
    phase: done
    terminal: true

transitions:
  pr_opened:
    from: implementing
    on: pull_request.verified_open
    when:
      pull_request:
        state: open
    to: awaiting_ci

  ci_satisfied:
    from: awaiting_ci
    on: ci.policy.evaluated
    when:
      ci:
        policy: merge_ready
        status: satisfied
    to: awaiting_review

  ci_failed:
    from: awaiting_ci
    on: ci.policy.evaluated
    when:
      ci:
        policy: merge_ready
        status: unsatisfied
    to: fixing

  fixes_pushed:
    from: fixing
    on: pull_request.head_changed
    to: awaiting_ci

  review_satisfied:
    from: awaiting_review
    on: review.policy.evaluated
    when:
      review:
        policy: required_review
        status: satisfied
    to: ready_to_merge

  review_unsatisfied:
    from: awaiting_review
    on: review.policy.evaluated
    when:
      review:
        policy: required_review
        status: unsatisfied
    to: fixing

  pull_request_merged:
    from: ready_to_merge
    on: pull_request.merged
    to: completed

commands:
  cancel:
    from: [implementing, awaiting_ci, fixing, awaiting_review, ready_to_merge]
    to: cancelled
    require_reason: true

ci:
  policies:
    merge_ready:
      all:
        - pipeline: github-pr
          checks: [lint, unit-tests]
        - pipeline: depot-container
          checks: [container-build]
      satisfied_for: current_pr_head

review:
  policies:
    required_review:
      all:
        - connection: github-reviews
          approvals:
            minimum: 1
      invalidate_on_new_head: true

delivery:
  pull_requests:
    required: true
    minimum: 1
    ci_policy: merge_ready
    review_policy: required_review
    completion: all_merged

events:
  ci.run.completed:
    clauses:
      - from: awaiting_ci
        when:
          all:
            - pipeline:
                equals: depot-container
            - conclusion:
                equals: failure
        assert:
          work.ci_failure_investigation_requested: true
        effects:
          - agent.task:
              prompt: Investigate the Depot CI failure.

The workflow is triggered per delivery item (for example, a linked issue or a manual start). Active build states such as implementing and fixing start durable agent.task effects via on_enter; waiting states such as awaiting_ci, awaiting_review, and ready_to_merge advance only on verified external events.

Core v2 concepts

states — State machine nodes. Every enabled state must declare a phase from setup, context, plan, build, test, pr, review, or done. Terminal states set terminal: true.

transitions — Directed edges between states. from is the source state (or list); on is an event kind; when is a predicate tree; to is the destination. Transitions for the same from + on must have disjoint when clauses.

commands — Operator-initiated transitions such as cancel or retry.

ci.policies and review.policies — Named policies referenced by delivery. CI policies list required pipeline checks; review policies list required approvals.

delivery.pull_requests — Dynamic delivery constraints. The hub verifies every PR through the workspace source-control connection before it becomes visible to the state machine.

events — Custom event definitions. Each event kind has ordered clauses matched by from state + when predicate.

Effects and facts

v2 workflows act through durable effects instead of chat markers. Allowed effects include:

agent.task — Start a durable agent task with a prompt.

exec.run — Run a shell command in the workspace. Requires the execution provider to grant execute_command.

dependency.update — Run a deterministic dependency update pass. Requires the execution provider to grant dependency_update and a repository with permissions: write.

ci.trigger, ci.retry, ci.cancel — CI pipeline actions. The CI connection must not have restricted the required capability.

issue.comment — Post a comment through a configured issue tracker connection.

Effects and hub adapters write facts into protected namespaces (ci.*, pull_request.*, review.*, effects.*, workflow.*, operator.*, exec.*). Workflows may only write work.* or custom.* facts with assert or set.

Predicate language

when and invariant clauses support a small, deterministic set of operators:

equals, not_equals, in, not_in, exists

all (conjunction) and any (disjunction)

Text markers such as [DONE] and message_contains are never trusted as control signals in v2. Use explicit events, transitions, and commands instead.

Converting from v1 to v2

Use the CLI conversion command to draft a v2 workspace or workflow. Review warnings and pair-validate the workspace + workflow before enabling.

bash
elasticclaw workspace convert .elasticclaw/workspaces/my-ws
elasticclaw workflow convert ./wf.yaml --workspace ./ws -o wf.v2.yaml

The converter produces a draft with warnings. Common mappings: v1 provider/nix/docker move to execution; v1 stages become v2 states; v1 entry: true becomes initial_state; v1 dependency_updates becomes the dependency.update effect; v1 cron triggers map 1:1 to v2 trigger.cron.

CLI commands

bash
elasticclaw workspace create --name my-app
elasticclaw workspace push my-app
elasticclaw workflow push --workspace my-app .elasticclaw/workflows/triage.yaml

elasticclaw workflow list --workspace my-app
elasticclaw workflow show triage --workspace my-app
elasticclaw workflow trigger triage --workspace my-app --input issue=ENG-123
elasticclaw workflow runs triage --workspace my-app --limit 20
elasticclaw workflow logs triage <run-id> --workspace my-app