Skip to main content

Lobster Workflows

Lobster is OpenClaw's official workflow shell: a small, typed (JSON-first) pipeline runtime with approval and input gates that can pause and resume. Instead of letting the agent work out a multi-step process every time, you write the sequence once in a workflow file and Lobster runs it the same way each time. The model is only called where a step explicitly asks for it.

  • Repository: openclaw/lobster, MIT, about 1,275 stars as of 2026-10-18
  • Runtime / CLI: npm @clawdbot/lobster (latest release v2026.9.16, 2026-10-01)
  • Gateway plugin: npm @openclaw/lobster (ships with each OpenClaw release, 2026.10.4 at the time of writing)
  • Official docs: docs.openclaw.ai/tools/lobster

This page tracks Lobster at commit 8ecbfe1 (2026-10-15). The syntax below comes from the Lobster source (src/workflows/types.ts, load.ts) and README. Lobster rejects unknown shapes at load time, so a workflow that loads is a good first test.


Why Lobster?​

Without LobsterWith Lobster
The agent decides the steps each timeSteps are fixed in a file
One model round trip per stepOne tool call for the whole pipeline
Side effects happen when the model decidesSide effects wait behind an explicit approval: step
A paused job has to be re-run from the startA paused job returns a resume token and continues from the gate
Hard to reviewThe file is data: easy to diff, review and graph (lobster graph)

Lobster is deliberately a small DSL, not a general scripting language. Each step can still call any CLI or script.


Installation​

Lobster runs in two modes.

Standalone CLI​

For development, cron-style command jobs and external pipelines:

npm install -g @clawdbot/lobster
lobster doctor
lobster run path/to/workflow.lobster

The package installs three executables: lobster, openclaw.invoke and its alias clawd.invoke.

Embedded Gateway plugin​

The @openclaw/lobster plugin runs workflows in-process inside the Gateway, using the same runtime. It is not installed or enabled by default.

openclaw plugins install @openclaw/lobster

Then allow the tool, either globally:

~/.openclaw/openclaw.json
{
tools: {
alsoAllow: ["lobster"],
},
}

or for one agent:

~/.openclaw/openclaw.json
{
agents: {
entries: {
main: {
tools: { alsoAllow: ["lobster"] },
},
},
},
}

alsoAllow adds lobster on top of the active tool profile without restricting other tools. The tool is disabled entirely in sandboxed tool contexts. See Lobster plugin reference.

Embedded mode and openclaw.invoke

In embedded mode, openclaw.invoke does not inherit the Gateway URL or auth for nested tool calls, so steps that call back into the Gateway with it are not reliable there (upstream openclaw#78736). Use openclaw.invoke from the standalone CLI with OPENCLAW_URL set, or call the tool directly outside Lobster.


Workflow syntax​

Workflow files are YAML or JSON (.lobster, .yaml, .yml, .json).

Basic structure​

workflows/hello.lobster
name: hello
description: Greets someone
args:
greeting:
default: Hello
description: Word to greet with
env:
MY_VAR: some-value
steps:
- id: say-hello
run: echo "$LOBSTER_ARG_GREETING, World!"
- id: show-date
run: date "+%Y-%m-%d"

Run it with an argument override:

lobster run --file workflows/hello.lobster --args-json '{"greeting":"Hi"}'

Top-level fields​

FieldTypeDescription
namestringWorkflow name
descriptionstringOptional description
argsobject{ <name>: { default, description } }
envobjectEnvironment variables for every step
cwdstringWorking directory for every step
stepsarrayNon-empty, ordered list of steps
cost_limitobject{ max_usd, action: warn | stop } (see Cost tracking)

Arguments and step references​

SyntaxWhereResolves to
${name}command text, env: values, cwd, workflow_argsThe arg value, by raw string replacement
$LOBSTER_ARG_<NAME>inside the shellThe arg value as an env var (name uppercased, other characters become _)
$LOBSTER_ARGS_JSONinside the shellAll resolved args as one JSON string
$step.stdoutstdin:, env:, when:, command textThe step's standard output
$step.json / $step.json.fieldsameThe parsed JSON output, or one field of it
$step.approved, $step.skipped, $step.error, $step.errorMessagesameApproval and error state

There is no {{ … }} template engine and there are no filters. Text in double braces is passed through literally.

Shell safety

${name} is a raw string replace into the shell command. An arg containing quotes, $, backticks or newlines can change the command. For anything that might come from a user or another system, read the value from the environment instead:

args:
text:
default: ""
steps:
- id: safe
run: jq -n --arg text "$LOBSTER_ARG_TEXT" '{"result": $text}'

The same applies to step output: run: echo "$prev.stdout" pastes the output into the command. Pass data with stdin:, or map it into a variable with env: (env values are not shell-expanded), and quote the variable.


Step types​

Each step has an id and exactly one of run/command, pipeline, workflow, parallel or for_each, or is an approval or input gate.

Shell commands (run: / command:)​

run: and command: are equivalent; run: is the preferred spelling.

steps:
- id: list-files
run: ls -la
cwd: /path/to/dir

- id: run-tests
run: npm test
timeout_ms: 60000

Native pipeline stages (pipeline:)​

Use pipeline: for Lobster commands that are not shell executables, such as llm.invoke and openclaw.agent. The prompt goes in --prompt; stdin: passes earlier output as artifacts.

steps:
- id: diff
run: git diff --stat HEAD~1

- id: summarize
pipeline: llm.invoke --prompt "Summarize this change in three bullets."
stdin: $diff.stdout

Approval gates (approval:)​

An approval step halts the workflow. In an interactive terminal Lobster asks on the TTY. Otherwise (tool mode, the embedded plugin, a scheduled job) it returns needs_approval with a resume token and the run ends at the gate.

The short forms are approval: required or approval: "Prompt text". Identity checks must be nested inside the approval object:

steps:
- id: review-deploy
approval:
prompt: Deploy to production?
required_approver: ops-lead
require_different_approver: true
stdin: $build.json
Field (under approval:)Description
promptText shown with the request
preview, itemsOptional material for the approver
initiated_byInitiator id; defaults to LOBSTER_APPROVAL_INITIATED_BY
required_approverThe approver id must match exactly
require_different_approverThe approver id must differ from the initiator

The approver id is read from LOBSTER_APPROVAL_APPROVED_BY at resume time. Lobster compares ids; it does not authenticate them, so treat this as a separation-of-duties check and keep the resume token private. If you put these fields beside approval: instead of under it, they are ignored and the check does not happen.

Input gates (input:)​

An input step asks a structured question and resumes with an answer that must match a JSON Schema:

steps:
- id: feedback
input:
prompt: What feedback should be included?
responseSchema:
type: string

The run returns needs_input; resume it with lobster resume --token <token> --response-json '"Shorten the intro."'. Read the answer later as $feedback.response.


Control flow​

Conditions​

when: (or condition:) gates a step on earlier results:

steps:
- id: branch
run: git branch --show-current | tr -d '\n'

- id: deploy-staging
run: ./deploy.sh staging
when: $branch.stdout == 'main'

- id: not-main
run: echo "Not on main, skipping deploy"
when: $branch.stdout != 'main'

Operators: ==, !=, <, >, <=, >=, &&, ||, !, and parentheses. Conditions can reference steps but not ${args}. To branch on an arg, add a step that prints it (printf '%s' "$LOBSTER_ARG_TARGET") and compare that step's stdout. Strip trailing newlines from command output before comparing, as above.

Parallel steps​

parallel is an object with a branches list. Each branch has an id and one of run, command or pipeline:

steps:
- id: checks
parallel:
wait: all # or "any": keep the first branch to finish, cancel the rest
timeout_ms: 600000
branches:
- id: lint
run: npm run lint
- id: typecheck
run: npm run typecheck
- id: test
run: npm test

- id: report
run: echo "all checks passed"

With wait: all, one failing branch fails the step. Branch results are available as $lint.stdout and so on, and $checks.json holds an object keyed by branch id.

For-each loops​

for_each takes a step reference that resolves to an array and runs its nested steps once per item, in order (not concurrently). The current item is $item (rename with item_var) and its position is $index (index_var):

args:
org:
default: my-org
steps:
- id: repos
run: gh repo list "$LOBSTER_ARG_ORG" --json name --limit 20

- id: audit-each
for_each: $repos.json
item_var: repo
batch_size: 5
pause_ms: 2000
steps:
- id: alerts
env:
REPO_NAME: $repo.json.name
run: gh api "repos/$LOBSTER_ARG_ORG/$REPO_NAME/dependabot/alerts" --jq length
FieldDescription
for_eachStep reference to an array
item_var / index_varNames for the item and index (default item, index)
batch_sizeGroup size for pauses: after every batch_size items, wait pause_ms
pause_msPause between batches (useful for rate limits)

Loop sub-steps cannot be approvals, inputs, workflows, parallel steps or nested loops; put a gate after the loop. The loop's $audit-each.json is an array with one object per item.

Sub-workflows​

A workflow: step runs another workflow file (relative paths resolve from the calling file). Pass args with workflow_args:

steps:
- id: lint-and-test
workflow: ci.lobster
workflow_args:
target: src/
on_error: continue

- id: deploy
workflow: deploy.lobster
when: "!$lint-and-test.error"

A workflow that would call itself, directly or through another file, fails with a cycle error. Step results have no exitCode; use .error, .errorMessage and .skipped.

info

Loop semantics such as maxIterations or shell-condition loops were proposed in lobster#20, which was closed without merging. Use for_each.


Error handling​

Retry​

steps:
- id: flaky-api
run: curl -fsS https://api.example.com/data
retry:
max: 3
delay_ms: 1000
max_delay_ms: 10000
backoff: exponential
jitter: true
FieldDescription
maxMaximum attempts (positive integer)
delay_msBase delay between attempts
max_delay_msCap on the delay
backofffixed or exponential
jitterAdd random variation to delays

Steps that dispatch to OpenClaw (openclaw.invoke, clawd.invoke, openclaw.agent) are not retried once dispatch was attempted, because the remote action may already have happened. Check the outcome before re-running by hand. On a for_each step, a retry restarts the whole loop, so only enable it for work that is safe to repeat.

Timeout​

steps:
- id: slow-task
run: ./long-running-script.sh
timeout_ms: 300000 # 5 minutes

On-error policies​

PolicyBehaviour
stop (default)Halt the workflow
continueRecord $step.error / $step.errorMessage and go on
skip_restSkip the remaining steps

LLM steps​

llm.invoke​

steps:
- id: read-code
run: cat src/auth.ts

- id: review
pipeline: >
llm.invoke --prompt "Review this code for security issues.
Return JSON with a findings array."
--output-schema '{"type":"object","properties":{"findings":{"type":"array"}},"required":["findings"]}'
stdin: $read-code.stdout

Provider resolution: --provider on the llm.invoke line, then LOBSTER_LLM_PROVIDER, then auto-detect from the environment.

ProviderEnv varsNotes
openclawOPENCLAW_URL, OPENCLAW_TOKENCalls the Gateway's llm-task tool, which must be enabled
piLOBSTER_PI_LLM_ADAPTER_URLUsually supplied by the Pi extension
httpLOBSTER_LLM_ADAPTER_URLGeneric HTTP adapter

To use the openclaw provider, enable llm-task as well:

~/.openclaw/openclaw.json
{
plugins: {
entries: {
"llm-task": { enabled: true },
},
},
tools: {
alsoAllow: ["lobster", "llm-task"],
},
}

See LLM Task.

openclaw.agent​

To hand work to a configured OpenClaw agent (its model, session and auth) rather than make a bare model call:

steps:
- id: logs
run: journalctl -u myapp --since "1 hour ago" --no-pager | tail -n 200
- id: triage
pipeline: openclaw.agent --agent ops --prompt "Summarize these logs and flag errors."
stdin: $logs.stdout

It delegates to the installed openclaw agent CLI and accepts --agent, --session-key, --session-id, --model, --thinking, --timeout and --local.

Cost tracking​

name: research
cost_limit:
max_usd: 20
action: stop # or "warn"
steps:
- id: design
run: cat docs/design.md

- id: research
pipeline: llm.invoke --provider http --prompt "Outline the main risks of this design."
stdin: $design.stdout

After a run, _meta.cost lists tokens and estimated cost per step. Prices come from a built-in table plus LOBSTER_LLM_PRICING_JSON (a JSON string such as {"my-model":{"input":1.0,"output":2.0}}, USD per million tokens). Unknown models record tokens at zero cost with a warning.

warning

The openclaw provider does not return token usage, so cost_limit cannot see or stop spend made through it. Set budgets on the OpenClaw side as well (see Cost Management).


OpenClaw integration​

Calling OpenClaw tools: openclaw.invoke​

openclaw.invoke calls one Gateway tool over HTTP (/tools/invoke). --tool and --action are required; tool arguments go in --args-json. It is a real executable, so use it from run: steps.

args:
chat:
default: "123456789"
steps:
- id: tests
run: npm test
on_error: continue

- id: alert
when: $tests.error
env:
CHAT: ${chat}
run: >
openclaw.invoke --tool message --action send
--args-json "$(jq -nc --arg t "$CHAT" '{channel:"telegram", target:$t, message:"Build failed"}')"

Requirements:

  • OPENCLAW_URL must be set (for example http://127.0.0.1:18789) or pass --url. There is no default.
  • OPENCLAW_TOKEN (or --token) when Gateway auth is on. The ambient token is only sent to localhost, 127.0.0.1 or [::1]; for any other host pass --token explicitly.
  • The Gateway HTTP tool surface denies some tools by default (exec, cron, gateway, sessions_spawn and others). See Tools invoke API.

--each --item-key message maps each pipeline item into one call, for example ... | openclaw.invoke --tool message --action send --each --item-key message --args-json '{"channel":"telegram","target":"123456789"}' in an inline pipeline.

Running workflows from the agent (embedded plugin)​

With the plugin installed, the agent calls the lobster tool with a pipeline string or a workflow file path:

lobster tool call
{
"action": "run",
"pipeline": "workflows/deploy.lobster",
"argsJson": "{\"environment\":\"staging\"}",
"timeoutMs": 60000
}

If the run reaches a gate, the tool returns an envelope and the call ends there:

lobster tool result
{
"ok": true,
"status": "needs_approval",
"output": [],
"requiresApproval": {
"type": "approval_request",
"prompt": "Deploy to production?",
"items": [],
"resumeToken": "..."
}
}

The agent shows the prompt in chat, and after your decision it resumes with { "action": "resume", "token": "<resumeToken>", "approve": true } (or false to cancel). Earlier steps are not re-run. Defaults: timeoutMs 20000, maxStdoutBytes 512000. Resume state lives under ~/.lobster/state (override with LOBSTER_STATE_DIR). Anyone allowed to use the tool who holds a token can resume it.

Scheduled runs (Automations)​

HEARTBEAT.md is no longer read by OpenClaw, so do not put workflow triggers there. Use an Automation job instead.

openclaw automations is the current name for openclaw cron, and create is an alias of add. A workflow with no gates can run as a command job on the Gateway host, with no model call. The output is delivered to chat:

openclaw automations add "0 3 * * *" \
--name "Nightly system check" \
--command "lobster run --file $HOME/workflows/system-check.lobster" \
--announce --channel telegram --to "123456789"

A workflow with approval gates is better run by the agent, so the gate reaches you in chat:

openclaw automations add "every 6h" \
"Run the Lobster workflow workflows/security-scan.lobster with the lobster tool and report the result. If it needs approval, ask me." \
--name "Security scan" --agent main --session isolated --announce

A command job that hits a gate stops and prints the needs_approval envelope; resume it with lobster resume --token <token> --approve yes.


Visualization​

lobster graph --file workflows/deploy.lobster # Mermaid (flowchart TD), the default
lobster graph --file workflows/deploy.lobster --format dot # Graphviz DOT
lobster graph --file workflows/deploy.lobster --format ascii # terminal-friendly list
lobster graph --file workflows/deploy.lobster --args-json '{"environment":"production"}'

Steps become nodes, stdin: references and when: conditions become edges, and approval gates are drawn as diamonds in Mermaid and DOT.


Real-world patterns​

Deploy pipeline​

workflows/deploy.lobster
name: deploy
args:
environment:
default: staging
steps:
- id: target
run: printf '%s' "$LOBSTER_ARG_ENVIRONMENT"

- id: lint
run: npm run lint

- id: test
run: npm test
timeout_ms: 300000
retry:
max: 2
delay_ms: 5000

- id: build
run: npm run build

- id: approve-deploy
when: $target.stdout == 'production'
approval:
prompt: Deploy to production?
required_approver: ops-lead
require_different_approver: true

- id: deploy
when: $target.stdout != 'production' || $approve-deploy.approved
run: ./scripts/deploy.sh "$LOBSTER_ARG_ENVIRONMENT"

- id: healthcheck
when: "!$deploy.skipped"
run: curl -fsS "https://$LOBSTER_ARG_ENVIRONMENT.example.com/health"
retry:
max: 5
delay_ms: 10000
backoff: exponential

environment only ever reaches the shell through $LOBSTER_ARG_ENVIRONMENT. Restrict it in deploy.sh to known values.

Code review chain​

Two reviews in parallel, then a synthesis and a merge gate:

workflows/code-review.lobster
name: code-review
args:
branch:
default: feature/new-auth
steps:
- id: get-diff
run: git diff "main...$LOBSTER_ARG_BRANCH"

- id: reviews
parallel:
wait: all
branches:
- id: correctness
pipeline: llm.invoke --prompt "Review this diff for correctness, edge cases and bugs."
stdin: $get-diff.stdout
- id: security
pipeline: llm.invoke --prompt "Review this diff for vulnerabilities, injection risks and auth issues."
stdin: $get-diff.stdout

- id: synthesize
pipeline: llm.invoke --prompt "Combine these two reviews into one assessment with action items."
stdin: $reviews.json

- id: approve-merge
approval:
prompt: Merge this branch?
stdin: $synthesize.json

Remember that the diff is untrusted input to the model. Keep this workflow read-only up to the gate.

System monitoring​

workflows/system-check.lobster
name: system-check
steps:
- id: disk
run: df -h --output=pcent,target | awk 'NR>1 && int($1)>85'

- id: docker
run: docker ps --filter "health=unhealthy" --format '{{.Names}}'

- id: report
when: $disk.stdout != '' || $docker.stdout != ''
env:
DISK: $disk.stdout
DOCKER: $docker.stdout
run: printf 'Disk over 85%%:\n%s\nUnhealthy containers:\n%s\n' "$DISK" "$DOCKER"

The {{.Names}} here is Docker's own Go template, not Lobster syntax. Before scheduling it as a command job (see Scheduled runs), run it once by hand to see exactly what lobster run prints, because that output is what reaches chat. Then trigger the job once with openclaw cron run <jobId>.


CLI reference​

CommandDescription
lobster run <file> / lobster run --file <file>Run a workflow file
lobster run --file <file> --args-json '{"k":"v"}'Run with args
lobster run --dry-run --file <file>Validate and print the plan without running anything
lobster run --mode tool '<pipeline>'Print a single JSON envelope (for integrations)
lobster '<pipeline>'Run an inline pipeline, for example lobster 'exec --json "echo [1,2,3]" | json'
lobster resume --token <t> --approve yes|noContinue or cancel at an approval gate
lobster resume --token <t> --response-json '<json>'Answer an input gate
lobster resume --token <t> --cancelCancel a paused run
lobster graph --file <file> [--format mermaid|dot|ascii]Visualize a workflow
lobster doctorCheck the installation
lobster versionPrint the version
lobster help <command>Help for one pipeline command

Named workflows run as a pipeline command: lobster run --mode tool "workflows.run --name github.pr.monitor.notify --args-json '{\"repo\":\"openclaw/lobster\",\"pr\":167}'". workflows.list lists them.

Pipeline commands​

CommandDescription
execRun an OS command (--json parses stdout, --stdin raw|json|jsonl feeds input)
where, pick, head --n NFilter, select fields, take the first N items
json, tableRender output
approve --prompt "…"Inline approval gate (--preview-from-stdin --limit N attaches a preview)
askInline structured question
llm.invoke, openclaw.agent, openclaw.invokeModel, agent and tool calls (above)
state.get, state.set, diff.lastSmall persisted state, for "only notify on change"

Configuration​

Environment variables​

VariableDescription
OPENCLAW_URLGateway URL for openclaw.invoke and the openclaw LLM provider. No default; for example http://127.0.0.1:18789
OPENCLAW_TOKENGateway bearer token (sent ambiently only to loopback hosts)
LOBSTER_LLM_PROVIDERopenclaw, pi or http
LOBSTER_LLM_ADAPTER_URLGeneric HTTP LLM endpoint
LOBSTER_PI_LLM_ADAPTER_URLPi adapter endpoint
LOBSTER_LLM_PRICING_JSONPricing overrides as a JSON string
LOBSTER_STATE_DIRResume-state directory (default ~/.lobster/state)
LOBSTER_MAX_OUTPUT_BYTESOptional per-stream, per-process output cap (unset means unlimited)
LOBSTER_MAX_HTTP_RESPONSE_BYTESOptional cap on HTTP response bodies
LOBSTER_APPROVAL_INITIATED_BY / LOBSTER_APPROVAL_APPROVED_BYIdentity ids for approval checks

Safety​

Lobster steps run shell commands as the user who runs Lobster: on your machine for the CLI, inside the Gateway process for the embedded plugin, and on the Gateway host for a command job. Treat a workflow file like a script you are about to run.

  • Put every send, post, delete or deploy behind an approval: step.
  • Pass untrusted values through $LOBSTER_ARG_*, stdin: or env:, never through ${arg} or $step.stdout in command text.
  • Keep tokens out of workflow files; Lobster does not manage secrets, it calls OpenClaw tools that do.
  • Automation command jobs run as operator-admin automations and are not covered by agent exec approvals.

See Security hardening for approvals, sandboxing and tool allowlists.


Debugging​

lobster doctor
lobster run --dry-run --file workflow.lobster
lobster graph --file workflow.lobster --format ascii
lobster version
ProblemCauseFix
openclaw.invoke requires --url or OPENCLAW_URLNo Gateway URLExport OPENCLAW_URL or pass --url
openclaw.invoke: connection refusedGateway not running or wrong portopenclaw gateway status
Nested openclaw.invoke fails in the pluginEmbedded mode does not pass Gateway authRun the workflow with the standalone CLI
... parallel must be an objectOld parallel: true syntaxUse parallel: { branches: [...] }
Workflow cost_limit.max_usd must be a non-negative numberOld warn/stop keysUse cost_limit: { max_usd, action }
... creates a cycleA sub-workflow calls back into a running fileRestructure the composition
Literal {{ ... }} in outputWrong template syntaxUse ${arg} / $step.stdout, or env vars
lobster runtime timed out (plugin)timeoutMs exceededRaise timeoutMs or split the pipeline

See also​