indra CLI
Nodes and orchestration
Run agents on this machine and over SSH, and chain them into workflows.
Agents can run on this machine and on any machine you can SSH into. A node is one such
machine; local is always there.
[local]
max_parallel = 4 # agents at once on this machine
[[node]]
name = "build-box"
ssh = "ci@build-box.internal" # host, user@host, or a Host alias from ~/.ssh/config
max_parallel = 2
targets = ["/srv/api", "/srv/web"] # codebases on that machine
Each remote node needs indra on its PATH (or set indra = "/path/to/indra"), and
ssh <destination> must work without a password prompt (keys or an agent). Then:
$ indra nodes
local ok laptop (macos aarch64, 10 cpus, indra 0.1.0, max 4 agents) runners: claude
build-box ok ci-7 (linux x86_64, 16 cpus, indra 0.1.0, max 2 agents) runners: claude, codex
gpu-box DOWN me@gpu-box ssh: Could not resolve hostname gpu-box: Name or service not known
In the console, Nodes shows each machine, its runners, its capacity and the review runs on
its disk (over SSH, or over the link for a machine that dials in). Click a remote run to copy its
text files to the hub (<data dir>/fetched/<node>/<run>) and open it like a local one: findings,
triage, comparison, memory, publish and post work on the copy, and changes stay in the copy.
Launch can start an agent on any node. A remote agent streams into the same
live view as a local one, with its subagents and tool calls, and Stop stops it on the remote.
Machines that dial in (no SSH)
A node can connect to the hub instead of being reached over SSH. On the Nodes page, Add a machine gives a one-time command; run it on that machine (with indra installed):
indra node connect https://hub.example:7878 --name build-box --code ABCDE-FGHJK --target /srv/api
# later starts: indra node connect https://hub.example:7878 --name build-box --target /srv/api
- Outbound only. The node opens a WebSocket to the hub (
/agent/connect); it needs no open port and no SSH account. The hub needs to be reachable by the node: bind it with--bind(and--allow-remote-configif you also want to edit settings remotely), ideally behind a TLS reverse proxy so the node useshttps:///wss://. - Enrolment. The code works once, for 15 minutes. The node then gets its own random key,
stored in the hub's encrypted secret store (never given to agents) and in an owner-only
agent.jsonin the node's config folder. Direct when possible. The WebSocket carries signaling; hub and node then open a WebRTC data channel (DTLS-encrypted) and job traffic moves onto it. On the same network that needs nothing; across NATs add STUN servers (
[server] ice_servers = ["stun:stun.example.org:3478"], or Settings > General) and TURN servers with their credentials:[[server.ice]] urls = ["turn:turn.example.org:3478", "turns:turn.example.org:5349"] username = "indra" credential = "secret:turn" # resolved on the hub, sent to nodes over their linkWhen there is no direct path, traffic is relayed over the WebSocket.
[server] webrtc = false, orindra node connect --no-webrtc, keeps everything on the relay. The Nodes page shows which path each node uses.- Reconnects on its own. Heartbeats every 5 s; a link silent for 15 s is dead. The node retries with exponential backoff and jitter (1 s up to 60 s). Jobs keep running through a drop: their output is numbered, the node keeps what the hub has not acknowledged and sends it again, and the hub drops duplicates, so each line arrives once, in order.
- No orphans. The hub fails a node's jobs if it stays away for 2 minutes; the node stops its jobs if the hub is gone for 10 minutes, or comes back as a different hub process.
- SSH stays as the fallback. A
[[node]]with ansshdestination is used over SSH whenever it has no live link.
The node runs only runners from its own configuration, in directories that exist on it, exactly
as with SSH. Builds without the webrtc feature (cargo install --no-default-features) relay
everything over the WebSocket.
Workflows
A workflow is a graph of agent tasks, each on a node, run in dependency order:
[[workflow]]
name = "nightly"
description = "Review the services on the build box and the web app here, then summarise."
[[workflow.task]]
id = "api"
node = "build-box"
target = "/srv/api"
prompt = "/indra full security review of {target}"
retries = 1 # one more attempt if it fails
[[workflow.task]]
id = "web"
node = "local" # the default
target = "../web" # local targets are relative to indra.toml
prompt = "/indra full security review of {target}"
[[workflow.task]]
id = "summary"
target = "."
prompt = "Summarise tonight's reviews and list new critical findings."
needs = ["api", "web"] # runs only if both succeed
- A task starts when everything it
needshas succeeded; if one of them failed, it is skipped. - Tasks start only while their node has room (
max_parallel, counting every agent there). - A failed task is retried up to
retriestimes. - Cancelling stops the running agents and cancels what has not started.
{target}and{node}are filled into the prompt;runnerdefaults toclaude.- Workflows are checked when the config loads: unique task ids, known nodes,
needsthat exist, and no cycles.
Run one from the terminal (cron, CI) or from the console's Workflows page:
$ indra run nightly
17:01:29 workflow 'nightly' started
17:01:29 api build-box running (process #1, attempt 1)
17:01:29 web local running (process #2, attempt 1)
17:14:52 web local succeeded
17:21:07 api build-box succeeded
17:21:07 summary local running (process #3, attempt 1)
17:23:40 summary local succeeded
17:23:40 workflow 'nightly' succeeded
Inputs. A workflow can ask for values when it runs, and its tasks use them as
{input.NAME} in their prompt, target, profile or model:
[[workflow]]
name = "triage-pr"
[[workflow.input]]
name = "pr"
label = "Pull request"
required = true
pattern = '[0-9]{1,9}' # text inputs may require a form
[[workflow.input]]
name = "target"
kind = "target" # one of the harness's targets
[[workflow.input]]
name = "depth"
kind = "choice"
options = [{ value = "", label = "auto" }, { value = "--tier=2", label = "standard" }]
[[workflow.input]]
name = "dry_run"
kind = "bool"
default = "true"
when_true = "--dry-run" # what {input.dry_run} becomes when ticked
[[workflow.input]]
name = "profile"
kind = "profile" # a configured profile, or none
[[workflow.task]]
id = "review"
target = "{input.target}"
profile = "{input.profile}"
prompt = "/sh-pr-review {input.pr} {input.depth} {input.dry_run}"
Kinds are text (the default), choice, bool, target and profile. Each value is checked
when the workflow starts: required ones present, text matching its pattern and on one line,
choices and targets among their options, profiles configured. An empty input leaves no gap in
the prompt.
Built in: with the indra harness, two workflows are always there (a configured workflow of the same name replaces one): pr-review (pull request, checkout, profile, depth, fail-on, deep, dry run; dry run is on by default) and security-review (codebase, optional focus, profile). Run any workflow from Runs > Run a workflow or the Workflows page (both ask for its inputs), or from the terminal:
indra run pr-review --input pr=https://github.com/acme/shop/pull/7 --input target=$PWD --input depth=--tier=2
indra run # lists the workflows and their inputs (* required)
The graph editor. Workflows > New workflow (or Edit graph on a workflow) opens a
canvas: drag tasks, skills, saved tasks and other workflows in from the palette (or select one
and press Enter), drag from a step's right edge to another step to make it run after the first,
and edit the selected step on the right. Remove a step with its Delete step button, the x on
the selected step, or the Delete key (Undo brings it back); remove an arrow by selecting it
and pressing Delete. Saving writes the same [[workflow]] TOML, with each step's position, so
indra run and the text editor keep working.
Skills as steps. A step can run one of the harness's skills instead of a free prompt: drag it
from Skills in the palette, or write skill = "<name>". For Claude Code it runs as the
skill's slash command, /<skill> <prompt>, so prompt holds the skill's arguments (default
{target}); other runners are told to use the skill.
[[workflow.task]]
id = "recon"
skill = "sh-security-review"
prompt = "{target} stage:recon" # becomes: /sh-security-review {target} stage:recon
Saved tasks ([[component]] in the file) are tasks kept for reuse: a task, a skill step or a
prompt with its profile, runner, node and target, that any workflow can drag in. A step that
starts from one overrides any field, and changing the saved task changes every workflow that
uses it. The editor's Save for reuse turns a step into one. (A task is one step of one
workflow; a saved task is a template for steps. Where a step runs is its node and
target, the repository or folder.)
[[component]]
name = "review"
prompt = "/indra full security review of {target}"
profile = "worker"
retries = 1
[[workflow.task]]
id = "api"
component = "review"
node = "build-box"
target = "/srv/api"
Workflows as code. A workflow can live in the repository it works on, as
.indra/workflows/<name>.toml, to be reviewed, committed and shared like any other code. Every
console and indra run with that repository as a harness root or target picks it up, and a
git pull that changes the files is seen without a restart.
# api/.indra/workflows/triage.toml
[[workflow]]
name = "triage"
[[workflow.task]]
id = "recon"
skill = "sh-security-review"
prompt = "{target} stage:recon"
profile = "planner" # a built-in profile, so it works for everyone
[[workflow.task]]
id = "lint"
component = "lint" # a saved task kept in the same file
needs = ["recon"]
[[component]]
name = "lint"
prompt = "Run the linters on {target} and fix what they report."
- A step without a target runs in that repository; a relative target is inside it.
- The file holds
[[workflow]]and[[component]]tables, in the same format asindra.toml. - In the editor, Saved in chooses between this machine's
indra.tomland a repository (the file's path is shown); saving to a repository also copies in the saved tasks the workflow uses, so the file stands on its own. Moving a workflow between the two moves it. - A workflow in your
indra.tomlwith the same name wins. A file that cannot be used (bad TOML, or a profile or node only its author has) is listed on the Workflows page and never stops the console; the built-in profiles exist everywhere, so shared workflows that use them just work. - Saving to a repository needs the admin role, like other settings.
Workflows compose. A task with workflow = "<name>" runs that workflow as one step: its
first tasks start after the step's needs, and anything that needs the step waits for its last
tasks. Its tasks show up as <step>__<task> in the run. A workflow cannot include itself.
indra run exits 0 only if every task succeeded; Ctrl-C cancels the run and stops its agents.
indra run without a name lists the configured workflows.
The Workflows page also has fan-out: one prompt across any set of targets on any nodes, in parallel within each node's capacity. That is the quickest way to review ten repositories spread over three machines.
How remote execution stays safe
- SSH does the transport. No daemon, no open port: authentication, encryption and host-key
checking are SSH's. indra calls
sshwithBatchMode=yes, so it never prompts and never accepts an unknown host key. - Nothing you type reaches a remote shell. The only remote command is the fixed
indra node exec; the job (runner name, prompt, target) travels as JSON on stdin. A prompt containing$(...)is just text. - The remote decides what runs.
indra node execruns only runners from its own configuration, by name, in a directory that must exist on it. The console can launch only into a node's configuredtargets. - Stop means stop. The hub keeps the job's stdin open for its lifetime; when it closes (Stop, cancel, or a dropped connection), the node terminates the agent's whole process tree.
- Destinations are validated, so a config value cannot smuggle in an ssh option such as
-oProxyCommand=....
Remote nodes need a POSIX shell (Linux, macOS, BSD). local works everywhere.