Automation Engine Internals

The typed node contract, the resolution pipeline, the execution orchestrator, and runtime shape validation.

Overview

The Automation Engine is a typed, deterministic graph evaluator. This page explains how it works internally: how a node's contract is defined once and used everywhere, how the editor resolves dynamic ports, how the orchestrator executes a graph, and how runtime validation guarantees data flowing between nodes matches its declared type.

The Node Contract

Every node and every data type has a single canonical declaration, node ids, input/output ports, port types, and, for object types, the full set of properties they expose. This declaration is the source of truth for the entire engine.

A node port's type is one of:

  • A primitive, string, number, boolean, date
  • A complex object, ticket, user, state, transition, iteration, project, link, comment, tag, each with a declared property list
  • A generic, list<T>, or a type variable T for nodes, such as Extract Properties or If/Else, that operate on any type

Because the contract is declared in exactly one place, the editor's type checker, the server-side resolver, the runtime executors, and the validation layer all agree on the same shape. This is what eliminates the classic "I wired an id where an object was expected" class of bug.

Object outputs always carry the full object, never a bare id. When a trigger emits a ticket, downstream nodes receive a structured ticket they can read title, state, fields, and more from, not a sequence number they would have to resolve again.

Design-Time Resolution

Some nodes have ports that cannot be known statically. Extract Properties is the canonical example: its outputs depend entirely on the type of whatever you connect to it. Connect a ticket and it offers title, state, fields; connect a link and it offers the link's properties.

The editor computes these ports live through a resolution pass. As you wire the graph, each node's resolver looks at its config and the resolved types of its inputs and produces the concrete set of ports. Connection validation then only allows type-compatible edges. The same resolution is available server-side, so the backend and the editor never disagree about what a graph's ports are.

Execution: The Orchestrator

When a trigger fires, the orchestrator builds an execution plan and walks it:

Seed from triggers

The plan starts at the trigger nodes that match the event, then expands downstream breadth-first.

Resolve inputs from edges

For each node, the orchestrator maps upstream outputs onto the node's input ports by following the graph's edges, matching the exact source-handle to the exact target-handle.

Execute the node

The node's executor runs, producing its declared outputs. Expensive work is skipped for any output that nothing downstream consumes.

Gate on conditions

If an upstream condition evaluated false, the nodes on that branch are marked skipped and never run.

Loops: ForEach and Collect

ForEach is special: it is not a single computation but an orchestration. When the plan reaches a ForEach over a list<T>, the orchestrator runs the entire downstream sub-graph once per element, in parallel for throughput, binding the loop's item output to each element in turn.

Collect is the matching barrier: it accumulates one value per iteration and, once all iterations finish, materializes them into a single ordered list<T> that execution continues with.

Schedulers: Fork and Resume

Scheduler nodes, such as recurring cron and run after, split a run in time. When execution reaches one, the engine forks: it snapshots exactly the upstream outputs the deferred branch needs, writes a scheduled instance with the next fire time, and lets the current run complete. A background service later picks up the due instance, restores the snapshot, injects the scheduler's "go" signal, and resumes the downstream branch from that point. See Schedulers for the user-facing view.

Runtime Shape Validation

There is no implicit trust between nodes. After a node produces its outputs, a shape guard validates them against the declared contract before they travel down any edge. The guard enforces two rules:

  • No phantom keys: a node may not emit a property that its declared type does not include. Emitting a stray id or template_id that the type never declared is a violation.
  • No missing consumed keys: if a downstream node consumes a property, that property must be present in the producing node's output.

When a node violates its contract, the engine fails loud: it names the node, the port, and the expected versus actual shape, instead of letting a silent null propagate and surface as a mysterious failure three nodes later.

Outputs are produced lazily. The declared type lists every property a value can have; at runtime a node materializes only the properties actually consumed downstream. The guard checks that what is emitted is valid and that everything consumed is present, giving both strictness and efficiency.

Determinism and Testing

Because every node has a typed contract and the engine is a pure evaluator over that contract, automations are deterministic and testable. The engine ships with an extensive conformance suite that exercises each node across input combinations and asserts that runtime output shapes match their declared contracts exactly, so contract drift is caught at build time, long before it could reach a running automation.