Skip to main content

Wires and automations

A wire connects something that happens to something that should happen: when a trigger fires, run an action. Concepts -> Wires covers the idea and the composer; this page is the vocabulary a wire is actually made of, for when you write one through the API or read one back.

A wire is a stored binding. It carries a trigger, an action to run, an optional condition, an optional target, and optional arguments and message templates.

Triggers

A wire's trigger_type is one of:

  • event: fires on a named event. The event name goes in trigger_event, for example inventory.stock.low or purchases.order.arrived.
  • on-create, on-update, on-delete: fire on the lifecycle of a record.
  • schedule: fires on a repeating rule. The rule is an RRULE string in trigger_schedule (the iCal recurrence format), which is how "every Monday at 9" or "the first of the month" is expressed. This is the same machinery behind recurring tasks and scheduled reminders.
  • user-invoked: not automatic. It puts the action on a record as a button the user taps.

Actions

The action is identified by its id (inventory:adjust-stock, projects:mark-task-done, and so on). Each action declares which record kinds it applies to, so a wire can only be pointed at an action the target record supports.

Arguments passed to the action live in args. String argument values are rendered as templates before the action runs, so an argument can pull a value out of the event, for example {{event.delta}}.

Target: what the action runs on

By default an action runs on the record that triggered the wire. The target field changes that:

  • self (or unset): run on the source record. This is the common case.
  • { "rel": "...", "dir": "in|out", "kind": "..." }: walk the links from the source record and run the action once on each record it reaches. This is how a wire on one record acts on the records related to it.
  • none: run with no record context, for triggers that do not originate from a record (an inbound webhook). The action locates its own target from its arguments.

Conditions

A wire can carry a condition, checked each time it would fire; if the check fails, that firing is skipped silently (a condition doing its job is not an error). A condition is a structured predicate, not an expression language:

{
"all": [
{ "path": "event.newQty", "op": "lte", "value": 5 },
{ "path": "material", "op": "eq", "value": "PLA" }
]
}

Every clause in all must hold. A clause names a path into the same data the templates see (the target record's fields at the top level, plus an event.* block for the event payload), an operator, and either a static value or a valuePath that reads another live value.

The operators are:

OperatorMeaning
eq, neqequal / not equal
lt, lte, gt, gtenumeric or string comparison
contains, not_containssubstring match
empty, not_emptythe value is / is not present

Comparison is numeric when both sides parse as numbers, otherwise string, compared case-insensitively. A valuePath clause is how a wire fires only when a value changed: compare event.after.location_id against event.before.location_id with op: "neq".

Templates

A wire can render a message and its string arguments from template data. The data includes the target record's fields by name, plus an event block: {{event.delta}}, {{event.reason}}, and {{event.actor.display_name}} all resolve. Filters like | default: "..." and | slug work here too; see computed fields for the filter list. | relative is the exception: it only applies in computed fields, and in a wire it renders the raw value instead of a relative time.

Chains and the depth guard

An action can emit further events, and those can fire more wires, which is how a multi-step chain composes. To stop a runaway loop (a wire whose action re-triggers the same wire), the engine refuses to fire past a depth of 8, records one wire_depth_exceeded entry, and lets the chain settle. Deployed bundles chain at most two steps, so the ceiling sits far above any real use.

Seeing what fired

Every firing writes to the activity log: wire_fired on success, wire_failed when an action throws (the failure is logged, never re-raised, so events stay best-effort), and wire_depth_exceeded when the guard trips. Filter activity by action to see a wire's history.

Who can create them

Creating, editing, and deleting wires requires an editor, admin, or owner. A wire runs its action with the authority its author set up, so a member creating a record can legitimately trip an admin-authored wire; the privilege check sits at wire creation, not at firing.