Skip to main content

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.
That is the whole feature. It exists because until now every trigger in Crewship was hard-coded: a schedule could start a routine, a webhook could start a routine, but “an issue changed status” could not, and there was no place to say so.

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:
A typo is accepted by the API — the journal has 117 entry types and no closed registry to check against — so it produces a rule that is saved, listed, and never fires. Checking first costs one command.

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".
--payload-equals has the same silent-failure mode as a typo’d --event, and a nastier one: a key no emitter writes is accepted, and the rule then matches nothing, forever, with no error anywhere. Read one real entry before writing the predicate:
and match on a key you can see in its payload.

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:
Some actions are precise enough to match on their own — review_approved is one thing and one thing only:
Note that 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’s wake: 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:
One per hour, not one per drop. A rule that trips its cap ten thousand times must not write ten thousand rows saying so.
max_per_hour is a burst brake, not a quota. The counter is held in the running server’s memory, so restarting the server clears it — a server that restarts every ten minutes has no effective hourly cap.Use it to stop a runaway rule. Do not use it where the number itself has to hold, such as billing or a contractual limit.

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) shows N automations beside 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.
Both are absent when nothing applies. A routine no rule targets, and an issue no rule watches, show nothing extra at all.
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

Every run an automation causes carries the rule’s id and how many events folded into it in the run metadata, so a run can always explain why it exists.

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 routine in 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.