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.

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