Automations
An automation is one rule: when this kind of thing happens in this workspace, run that routine. It watches a single journal event type, evaluates a predicate against each entry, and parks a debounced run of a routine.What an automation can and cannot do
An automation can only enqueue — or, for the one rule type nothing but Pages writes today, open an issue. It never executes a routine inline and holds no veto over anything. That is a deliberate boundary, not a missing feature. Automations are matched on the journal write path — the moment an event durably commits — so anything they did synchronously would be latency added to every write in the product. Matching is in-memory; the only thing that reaches the database is a single row parked in the deferred-run queue, and even that happens on a background flush rather than inline.Automations are not hooks. A hook is an intercept:
crew-scoped, blocking, and able to refuse the thing it fired on. An
automation is workspace-scoped, non-blocking, and reacts after the fact.
They look similar on paper and guarantee opposite things, which is why
they are separate.
Anatomy of a rule
Choosing an event type
There is no wildcard by design: a rule that fires on “anything” is a support ticket waiting to happen. Pick the one type you mean, and confirm it exists before you save:The matcher
Every predicate you set must be satisfied; the ones you leave out are “don’t care”. Setting none means every entry of that type matches.--payload-equals parses its value as JSON when it can, so count=3 matches
the number 3 and action=status_changed matches the string
"status_changed".
What mission.status_change actually carries
Every issue event goes through one emitter. It writes two keys on every
event, plus two more on a status transition:
So “fire when an issue moves to DONE” is one predicate:
from and to appear only on a transition. A key that were always present
and sometimes empty would be a predicate that silently matched nothing, which
is the failure automation preview exists to surface.
To react to any status change rather than one target, match the action and let
the routine decide:
review_approved is
one thing and one thing only:
mission.status_change is the CATCH-ALL issue entry type, not
“status changed specifically” — created, assignee_changed, commented and
mentioned have their own entry types, everything else lands here. That is why
the action predicate is usually needed and not redundant.
Rules a Page wrote
A page panel’swake: gate compiles to a rule in this same table, named
page <slug>/<panel> wake <n>, with action_kind: issue. They appear in
crewship automation list like any other rule, which is the point — a gate you
cannot see is a gate you cannot debug.
They are derived state: the page spec owns them, every save of that page
rewrites them, and deleting the page deletes them. Disabling or editing one
here does not stick. Remove the gate from the page instead.
Their event type is page.panel.updated, which every accepted panel push
emits, and it carries:
Inputs and the event namespace
Input values are rendered with the same template renderer routine steps
use, against the entry that triggered the rule:
An unresolvable reference renders empty, exactly as it does inside a routine.
Burst control
Two independent controls, and they do different jobs.debounce_seconds collapses a storm into one run. Two hundred status
changes on the same issue inside the debounce window produce a single run
carrying the most recent event’s inputs, not two hundred runs. The parked run
also has a ceiling — debounce_seconds × 10 from the first match — so a
stream of events that never stops still fires at the ceiling instead of being
pushed out forever, and the next match after it starts a fresh run.
Events collapse together only when they are about the same subject. The
subject is the most specific identity the entry carries: the issue, else the
run, else the agent, else the crew. So fifty run.failed entries for one run
are one triage run, but two different runs failing inside the window are two —
coalescing keeps the last event’s inputs, and folding unrelated subjects into
one run would silently act on whichever arrived last. An entry that carries
none of those identities is genuinely workspace-scoped and collapses to the
rule itself.
max_per_hour caps how many runs the rule may cause per rolling hour. It
is charged per run, not per matched event — a burst that coalesces into
one run costs one unit. Over the cap, matches are dropped and exactly one
automation.throttled entry is written to the journal for that hour:
Where rules show up in the UI
There is no automation management screen — the CLI above is the whole write surface — but the two pages a rule affects both say so, read-only:- A routine’s detail page (
/routines) showsN automationsbeside its status pills whenever a rule targets it, and lists them under Triggers → Automations: rule name, the event type it watches, and whether it is armed. A disabled rule is shown greyed rather than hidden — a rule that is switched off is usually the answer to “why did nothing happen”. - An issue’s detail page (
/issues) grows an Automations card listing the rules an event from that issue could set off.
The issue card lists rules that could fire, not rules that will. Only
mission_ids and crew_ids can be decided from an issue; agent_ids,
severities and payload_equals describe an event that has not happened yet
and narrow further at match time. A rule excluded by mission_ids or
crew_ids is never listed — that exclusion is provable.Watching a rule work
Permissions
Reading the list is available to any workspace member — “what fires here” is the first thing anyone debugging an unexpected run needs. Creating, editing and deleting are ADMIN or OWNER: a rule grants autonomous routine execution across the workspace, on events its author may never produce themselves.Deleting
Deletion is soft. The rule stops matching immediately and the row stays in the database, because a run it caused is partly explained by the rule that caused it — hard-deleting turns those runs into orphans.API
PATCH is sparse: only the fields present in the body are written, so
toggling enabled cannot clobber a matcher somebody edited a moment ago.
Full flag reference: crewship automation.
Limits
- One event type per rule. Compose several rules rather than asking for a wildcard.
- The action kind is
routinein this release. The stored shape leaves room for others; anything else is rejected at write time rather than silently accepted and never run. - A rule whose routine does not exist in the workspace is refused at create/update time, and one whose routine is deleted afterwards stops firing until the routine comes back.