Skip to main content

Automations

Overview​

The Automations module provides a visual workflow automation engine with a node-based editor. Users create automations composed of trigger nodes, condition nodes, and action nodes connected by edges. Automations can be triggered by platform events (e.g., a new follower triggers a chat message) or manually. The module includes an execution engine that walks the node graph, evaluates conditions, executes actions via a dispatcher, and supports template variables. A manual run reports its outcome inline as ExecutionResultGql (automationId, completed, steps, failedSteps, stepResults); runs are not persisted to an execution-history table.

Architecture​

Backend​

  • GraphQL (apps/api/src/graphql/automations.rs) -- Full CRUD for automations, nodes, and edges. Manual execution mutation that builds and runs the execution graph.
  • REST (apps/api/src/routes/automations.rs) -- parity endpoints under /v1/automations.
  • Database (apps/api/src/db/automations.rs) -- PostgreSQL operations for automations, automation_nodes, and automation_edges tables. Supports bulk replace for nodes and edges.
  • Execution Engine (crates/lo-automation/src/) -- ActionExecutor walks the execution graph from a trigger node, evaluates conditions, and dispatches actions. TemplateContext provides variable interpolation. NodeType enum defines available node types (triggers, actions, logic).
  • Dispatcher (apps/api/src/dispatch.rs) -- RedisActionDispatcher sends action payloads via Redis pub/sub for real-time execution. Spotify and moderation steps run in the API process through the dashboard's services instead (see Spotify and moderation steps).

Frontend​

  • Node-based visual editor with a drag-and-drop canvas for creating and arranging nodes.
  • Edge connections define the execution flow between nodes.
  • The toolbar's Action menu lists OBS Control (obs_control) and Streamer.bot (streamer_bot) with a Not available yet hint. They cannot be added, because no Lumio service carries these actions out yet (see Action delivery). The list lives in UNAVAILABLE_ACTION_TYPES (shared/automation/src/utils.ts).
  • A new Spotify node starts as Play and a new Moderation node as a 600-second Timeout (getDefaultConfig), so a node saved untouched carries what its panel shows. The Spotify panel asks for the volume (0–100) when the action is Volume; the Moderation panel offers Timeout, Ban and Delete and asks for the duration in seconds for a timeout.
  • An automation saved with one of these nodes still opens unchanged. The node shows a Does not run yet badge and its settings panel a notice that the step does not run yet; a run fails when it reaches the step.
  • Test run button in the editor header (/dashboard/automations/{id}/editor), shown to members with automations:execute; the editor itself sits behind feature:automation. Unsaved edits are saved first, because the server always runs the saved graph — if the save fails, the run does not start.
  • Automation settings in the editor header, shown to members with automations:edit, include On test events. Prefix is the default; Ignore prevents a test event from starting this automation. This setting applies to incoming test events, not to the editor's manual Test run.
  • The run goes through the web proxy POST /api/automations/{id}/execute (the executeAutomation mutation) and its result appears in a panel under the header:
    • Green only when at least one step ran and none failed. Any failed step, or a walk that did not complete, shows the run as failed with "n of m steps failed"; a run that executed no step (nothing connected to the trigger) is neutral, not green.
    • Every executed step is listed in order with its node, status (succeeded / skipped / failed), duration and — for a failed step — the server's error, e.g. No consumer for action control_obs. Selecting a step opens that node's settings.
    • On the canvas each executed node gets an outline in its step's colour (red = failed, green = succeeded, grey = skipped). A node that ran more than once (loop) shows its worst outcome. The outline is display-only and never marks the automation as changed.
    • A request-level error (for example No trigger node found in automation) is shown in the same panel. Dismissing the panel clears the outlines.

API​

GraphQL Queries​

Every automation query and mutation additionally requires the feature:automation feature flag on the account. When the flag is off the operation fails on both protocols before any permission check runs.

QueryPermissionDescription
automationsfeature:automation + automations:readList all automations for the account
automation(id: UUID!)feature:automation + automations:readGet a single automation with all its nodes and edges
automationWebhookUrl(installId: UUID!, automationId: UUID!)feature:automationWebhook URL + secret for an extension trigger node (see Webhook triggers)

GraphQL Mutations​

MutationPermissionDescription
createAutomation(input: CreateAutomationInput!)automations:createCreate a new automation with name and description. Returns AutomationGql!.
updateAutomation(input: UpdateAutomationInput!)automations:editUpdate automation name, description, enabled state or testEvents (prefix or ignore). Returns AutomationGql!.
deleteAutomation(id: UUID!)automations:deleteDelete an automation and all its nodes/edges. Returns AutomationDeleteResult!.
saveAutomationNodes(automationId: UUID!, nodes: [AutomationNodeInput!]!)automations:editBulk replace all nodes for an automation. Returns [AutomationNodeGql!]!.
saveAutomationEdges(automationId: UUID!, edges: [AutomationEdgeInput!]!)automations:editBulk replace all edges for an automation. Returns [AutomationEdgeGql!]!.
executeAutomation(id: UUID!)automations:executeManually execute an automation (the editor's Test run). Starts at the manual_trigger node, or at the earliest stored trigger when there is none, and runs it whether or not the automation is enabled (see Data Flow). Returns ExecutionResultGql! with completed, steps, failedSteps and per-step stepResults (nodeId, nodeType, status = success/skipped/error, error, durationMs).

Built-in node types​

nodeType on a saved node is one of the NodeType variants in crates/lo-automation/src/types.rs:

GroupNode types
Triggercommand_trigger, keyword_trigger, event_trigger, timer_trigger, manual_trigger, webhook_trigger, extension_trigger
Actionchat_message, discord_post, obs_control, streamer_bot, spotify_control, overlay_alert, webhook_call, moderation, set_variable, get_variable, extension_action
Logiccondition, delay, random, loop, extension_logic

The three extension_* variants are the dispatch points for extension nodes.

Input Types​

AutomationNodeInput:

FieldTypeDescription
idUUIDNode ID (client-generated)
nodeTypeStringNode type (trigger, condition, action)
positionXf64X position in the editor canvas
positionYf64Y position in the editor canvas
configJSONNode-specific configuration (default: {})

AutomationEdgeInput:

FieldTypeDescription
idUUIDEdge ID (client-generated)
sourceNodeIdUUIDSource node ID
targetNodeIdUUIDTarget node ID
sourceHandleStringSource handle name (default: "output")
targetHandleStringTarget handle name (default: "input")

REST Endpoints​

Mirror the GraphQL surface. All paths live under /v1/automations.

MethodPathPermissionDescription
GET/v1/automationsautomations:readList automations
POST/v1/automationsautomations:createCreate an automation
GET/v1/automations/{id}automations:readGet an automation (with nodes and edges)
PATCH/v1/automations/{id}automations:editUpdate name/description/enabled or test_events (prefix or ignore)
DELETE/v1/automations/{id}automations:deleteDelete automation + nodes + edges
PUT/v1/automations/{id}/nodesautomations:editBulk replace node list
PUT/v1/automations/{id}/edgesautomations:editBulk replace edge list
POST/v1/automations/{id}/executeautomations:executeRun the automation manually. Returns completed, steps, failed_steps and per-step step_results (same fields as GraphQL, snake_case)

Request bodies are snake_case copies of the GraphQL CreateAutomationInput / UpdateAutomationInput / AutomationNodeInput / AutomationEdgeInput.

WebSocket​

ChannelGateFeature flag
automations:{account_id}automations:read on the account in the channel keyfeature:automation

An account without feature:automation is rejected at subscribe time, so the stream cannot be reached by crafting the channel name. The gate lives in channel_gate_for / channel_feature_for (crates/lo-websocket/src/gate.rs).

Permissions​

PermissionDescription
automations:readView automations and their configuration
automations:createCreate new automations
automations:editEdit automation metadata, nodes, edges
automations:deleteDelete automations
automations:executeManually trigger and start/stop automations

There is no automations:history permission: no execution-history surface exists — executeAutomation returns its results inline and they are not persisted. The former automations:history account permission was removed in ZAF-1094 (it gated nothing).

Database​

TableDatabaseDescription
automationsPostgreSQLid, account_id, name, description, enabled, test_events (prefix by default), created_at, updated_at
automation_nodesPostgreSQLid, automation_id (FK), node_type, position_x, position_y, config (JSONB), created_at
automation_edgesPostgreSQLid, automation_id (FK), source_node_id, target_node_id, source_handle, target_handle, created_at

Data Flow​

An automation runs in one of two ways:

  • Manually: POST /v1/automations/{id}/execute (REST) or executeAutomation (GraphQL). This is the editor's Test run, and it returns the result inline. Both protocols apply the same rules:
    • The run starts at the manual_trigger node. Without one it starts at the earliest stored trigger of any kind (ties broken by node id), so a graph with several triggers always starts at the same node.
    • It runs disabled automations too, so a draft can be tested before it is switched on, and it works whatever the trigger switch says.
    • It runs with an empty template context: variables the trigger would supply ({{user}}, the event fields) have no value.
    • A graph without any trigger fails with No trigger node found in automation (REST 400 BAD_REQUEST, GraphQL error with the same message).
    • Whoever holds automations:execute can therefore run every action of every automation in the account, including actions such as moderation that need their own permission elsewhere. Grant automations:execute only to members who may run all of the account's automations. Saving such a node needs that permission (see Nodes that act as the channel owner).
    • A moderation step needs a target user from the trigger, so in a manual run it fails with Invalid config: Moderation needs a target user from the trigger context.
  • From a trigger: chat, keyword, command, event and timer triggers fire only when the operator has set [automation] triggers_enabled = true (LUMIO__AUTOMATION__TRIGGERS_ENABLED). The switch is off in every shipped config. With it off, trigger nodes are saved but never evaluated. See Installation → Automation triggers.

An incoming test event has event.raw.isTest: true. An automation with test_events: ignore does not start from it. Under the default prefix mode, chat_message and discord_post start their output with [Test] ; the moderation step is always skipped. overlay_alert, obs_control, spotify_control and streamer_bot steps run only when the test was sent with Show on stream; otherwise their step status is skipped. Webhooks, variables and extension nodes still run, and webhook and extension-node input includes event.raw.isTest. This is distinct from the editor's manual Test run, which executes a saved graph without an incoming event.

  1. User creates an automation and adds nodes (triggers, conditions, actions) via the visual editor.
  2. Nodes are connected with edges that define execution flow.
  3. Nodes and edges are saved via bulk-replace mutations (saveAutomationNodes, saveAutomationEdges).
  4. When the automation runs:
    • The execution graph is built from nodes and edges.
    • The executor starts at the trigger node and walks the graph.
    • Conditions are evaluated; actions are dispatched via RedisActionDispatcher (see Action delivery).
    • Each step is recorded with its status for the execution result.
  5. A manual run returns whether the walk completed, how many steps ran, how many failed, and each step's status and error. A triggered run publishes {automation_id, trigger_node_id, completed, steps_count, error} to lumio:automations:{account_id}.

Trigger path​

Twitch announcements follow the ordinary chat-message path and can match keyword and command chat triggers; there is no announcement-specific exclusion.

With triggers_enabled = true, every API replica runs the automation trigger supervisor (apps/api/src/workers/automation_triggers.rs). Only the replica that holds the Redis lease lumio:automation:triggers:leader runs workers, so a trigger fires once, not once per replica. The lease lives for 30 s and is renewed every 10 s. A replica that loses it stops its automation workers, and another replica takes over within one lease lifetime.

Every 10 s the leader starts or stops per-account workers to match the database. Accounts whose plan lacks feature:automation get no workers.

WorkerRuns forFires
Trigger worker (workers/automation.rs)Each account with an enabled automationSubscribes to lumio:chat:{account_id} and lumio:events:{account_id}. A chat message fires keyword_trigger and !command command_trigger nodes, and a stream event fires event_trigger nodes. A bot command with response_type = automation is published on the same chat channel as {"type": "command", "automation_id", …}, and runs that automation from its command_trigger node
Timer worker (workers/automation_timer.rs)Each account with an enabled timer_triggerChecks every second and fires each timer once per interval_seconds. A newly seen timer waits one full interval before its first fire, also after a restart or a leader change

Each account reloads its enabled automations at most every 5 s, so an edit takes effect within 5 s.

A Twitch Shared Chat partner message or event never fires a trigger. These are messages written in another channel's room and delivered into this one. They are still displayed. The Twitch bot marks such a bot command is_shared_chat. The bot does not publish it, and the trigger worker skips it too.

Command trigger gates​

A command_trigger runs only when both of its gates pass. They apply to a !command chat message and to a bot command with response_type = automation alike.

GateConfig fieldBehaviour
Rolepermission_level (everyone < follower < subscriber < vip < moderator < broadcaster, default everyone)The sender's role must be at least this level. For a chat message the role comes from the message (broadcaster badge, then is_mod, is_vip, is_sub). Follower status is not known from chat, so follower admits subscribers and above only. For a bot command it is the user.role the bot command registry sends. That gate applies on top of the bot command's own role gate. A missing or unknown sender role counts as everyone. An unknown stored permission_level admits only the broadcaster
Cooldowncooldown_seconds (default 0 = none, at most 86,400 s = one day)One run per trigger node per window, for all senders together. The slot is the Redis key lumio:automation_cd:{account_id}:{automation_id}:{node_id}, taken with SET NX EX cooldown_seconds once the run holds its slot in the account's execution cap, so a run dropped at the cap does not use up the cooldown. A run inside the window is skipped. If Redis is unreachable, the run is skipped too

The role gate runs first, so a sender below the level never uses up the cooldown. A blank keyword, command or alias never matches, whatever its match mode. Save-time validation rejects these values; a row saved before that rule stays inert.

Each triggered execution runs in its own task, so a long Delay node never holds up the next trigger:

LimitValueWhen exceeded
Executions per account at oncemax_concurrent_executions (default 8), trigger and timer runs togetherThe trigger is dropped and logged, not queued
Wall-clock time per executionexecution_timeout_secs (default 3900 s, above the 1-hour Delay cap)The run is stopped and publishes completed: false with Execution timed out after … seconds
Worker shutdown or lost lease—Running executions of that worker stop at once

Action delivery​

A step is success only when a consumer received the action, or — for Spotify and moderation, which run in the API process — when the service carried it out. Bot actions are published to lumio:bot:action:{platform}:{account_id} as a { "type": …, "data": … } envelope (lo_cache::bot_action::BotActionCommand); the API and every bot share that type and its type-name constants. A step fails with an error status when:

  • no Lumio service consumes the action (No consumer for action …), or
  • the Redis PUBLISH reached no subscriber, for example because the bot is not running (No subscriber received action …).
Action nodeDelivered toToday
chat_messageTwitch, Kick or YouTube bot (chat_message; platform defaults to twitch)Delivered. Any other platform fails with no consumer
discord_postDiscord bot (post_message)Delivered
overlay_alertAPI event relay → WebSocket automations:{account_id}Delivered
webhook_callThe configured URL over HTTP (see Webhook call targets)Fails with webhook_failed or webhook_http_error
set_variable / get_variableRedis keys (lumio:var:{account_id}:{name}, 30-day TTL refreshed on every write)Delivered
extension_action / extension_logicAutomation Worker over HTTPFails when the worker is unconfigured or the handler errors
spotify_controlservices::spotify_control in the API, with the channel's Spotify connectionDelivered. Fails when Spotify is not connected or the plan lacks feature:music
moderationservices::chat_moderation in the API, as the channel owner (Twitch, YouTube, Kick)Delivered. Fails when the owner has no valid login for the platform or the plan lacks feature:multichat
obs_control—Fails with no consumer
streamer_bot—Fails with no consumer

The editor offers neither of the two no consumer actions above (obs_control, streamer_bot); it marks them Not available yet.

Spotify and moderation steps​

Both steps call the services the dashboard uses, so a step behaves like the same click in the dashboard. Tokens come only from the central refresh helpers (get_fresh_connection_token for Spotify, the login-token service for moderation); a step never refreshes a token itself.

Spotify (spotify_control, config action and, for volume, volume_percent):

actionEffect
playResume playback
pausePause playback
nextSkip one track
previousGo back one track
volumeSet the volume to volume_percent (0–100)
  • The step uses the channel's Spotify connection; it needs no signed-in member. Without a connection it fails with Spotify not connected. Connect Spotify in your dashboard settings.
  • The node is mapped onto the extension actions' control table (services::extension_actions::spotify_control_from_params; previous, which has no extension action, maps directly), so an invalid value fails the same way: volume without volume_percent or above 100, or an unknown action, fails with Invalid config: ….
  • It records the same spotify:* event as the dashboard and extension actions, so the event feed and Spotify widgets show it. The event's raw carries via: "automation" and automation_id.

Moderation (moderation, config action, duration_seconds, reason):

  • The step moderates as the channel owner (accounts.owner_id), with the owner's own platform login, because an automation has no signed-in member. It runs on the platform of the trigger (channel.platform; twitch when the trigger names none).
  • timeout and ban target the user who triggered the automation (user.user_id); a timeout without duration_seconds lasts 600 seconds. delete removes the chat message that triggered it, so it needs a chat, keyword or command trigger. Any other action fails with Invalid config: Unsupported moderation action: ….
  • A refused action fails the step and never counts as a success: no owner (Channel has no owner), no owner login for the platform (No Twitch login connection. Connect your Twitch account first.), or a refusal with its code, e.g. login_reconnect_required: Your Twitch login must be reconnected. Log in with Twitch again.
  • A successful action writes the moderation log entry and broadcasts like a dashboard action. The log entry names the owner as moderator, and its details carry via: "automation", automation_id and automation_name. In the user card's Moderation history, the entry shows by ‹owner› · by automation ‹name›. Chat moderation writes no audit event, from the dashboard or from an automation.

Webhook call targets​

The webhook_call node sends its request from the Lumio API, so every target passes a target filter before a connection opens (apps/api/src/webhook_target.rs). The same filter runs for manual, event- and timer-triggered executions.

RuleDetail
Schemehttps or http only. Plain http stays allowed for self-hosted receivers without TLS; prefer https, because headers and body travel unencrypted otherwise
AddressThe host is resolved once. The target is refused when any address is not public: loopback (127.0.0.0/8, ::1, localhost), private (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7), link-local (169.254.0.0/16, fe80::/10), CGNAT (100.64.0.0/10), unspecified, multicast, broadcast and reserved ranges, and IPv6 forms that wrap one of these (IPv4-mapped, NAT64, 6to4)
PinningThe request connects only to the addresses that were checked; the client does no DNS lookup of its own, so a changed DNS answer cannot redirect it
RedirectsNever followed. A 3xx answer is a non-2xx answer
ProxySystem proxy settings are ignored for these requests
Timeouts10 s to connect, 60 s in total

Substituted template values in the URL, headers and body are encoded for their position (lo_automation::template::Sink): percent-encoding after the URL's literal authority, control characters in header values replaced by spaces, and JSON-string escaping or form encoding of the body by its Content-Type (a body template starting with { or [ counts as JSON when no Content-Type is set). Discord post messages escape markdown in substituted values. The full table is in Automation Templates → Output Encoding.

A failed webhook step reports only one of two fixed errors, on GraphQL executeAutomation and REST POST /v1/automations/{id}/execute alike:

ErrorMeaning
webhook_failedThe request was not delivered: the target was refused by the filter, the URL is invalid, the host did not resolve, or the connection, TLS handshake or timeout failed
webhook_http_errorThe target answered with a status outside 2xx

A refused target and an unreachable one return the same webhook_failed. The result never contains the URL, a status code or transport error text; the API logs the detail server-side (without the URL). A target refused during a manual run also writes an account:webhook_target_blocked audit event; event- and timer-triggered runs only log it, because they would repeat it on every trigger.

Credentials in webhook nodes​

Webhook nodes hold credentials for other services, so these config values are write-only:

Node typeWrite-only values
webhook_triggersecret
webhook_callevery value in headers, the userinfo of url (https://user:password@…), and every query and fragment value of url

Every read returns each of these values as "[redacted]": GraphQL automation and saveAutomationNodes, and REST GET /v1/automations/{id} and PUT /v1/automations/{id}/nodes. null and empty values stay as they are, so the editor can tell a set value from an unset one. Header names, the URL's host and path, and query keys stay readable.

On save (saveAutomationNodes / PUT /v1/automations/{id}/nodes), a "[redacted]" value means "keep the stored value". The API matches it to the stored node with the same id and node type: a header by name (case-insensitive), a query or fragment value by key and position, and the secret and the URL userinfo directly. A loaded graph can therefore be saved back unchanged, and editing one header leaves the others intact. A placeholder with nothing stored to keep (a new or copied node, a changed node type, a header that did not exist) fails the whole save with the same 400 on both protocols, Automation node <id>: the "[redacted]" placeholder in <field> has no stored value to keep; enter the value again, and writes nothing. Both protocols share apps/api/src/services/automation_secrets.rs.

Graph rules and limits​

Save-time validation​

saveAutomationNodes / PUT /v1/automations/{id}/nodes and saveAutomationEdges / PUT /v1/automations/{id}/edges validate the graph before anything is written. Both protocols run the same check (lo_automation::validate_graph, called from apps/api/src/services/automation_graph.rs) and reject with the same 400 BAD_REQUEST and message (Graph error: …).

RuleChecked on
Node ids are uniquenodes
random nodes have min <= maxnodes
timer_trigger nodes have interval_seconds of at least 60 (timer trigger …: interval_seconds must be at least 60)nodes
set_variable names are at most 128 bytes, and one automation writes at most 100 distinct namesnodes
At most 200 nodes (an automation can have at most 200 nodes) and 400 edges (… at most 400 edges)nodes / edges
A node's config is at most 16 KiB serialized (node …: config exceeds 16384 bytes)nodes
command_trigger: command and every alias are not blank, permission_level is one of everyone, follower, subscriber, vip, moderator, broadcaster (any case), cooldown_seconds is at most 86,400nodes
keyword_trigger: no keyword is blank. In regex mode each keyword is at most 200 characters and compiles (invalid regex otherwise)nodes
extension_* nodes reference an install of this account, for the extension named in extension_meta (Extension install not found for this account otherwise)nodes
No self-loop (an edge from a node to itself)edges
No duplicate edge (same source, target and source handle)edges
No edge to or from an unknown nodeedges
No directed cycle through a trigger nodeedges

Cycles between non-trigger nodes are allowed; the run-time budget bounds them. A Loop node repeats its subtree through its count, not through a back-edge.

Nodes that act as the channel owner​

A moderation node moderates with the channel owner's login, and a Spotify node drives the channel's Spotify connection. Saving one therefore needs the permission it acts with, on top of automations:edit, so a member cannot act as the owner beyond their own rights:

NodeNeeds
moderation with action banchat:ban
moderation with action timeoutchat:timeout
moderation with action deletechat:delete
moderation with any other actionchat:ban
spotify_controlspotify:playback
spotify_control with action volumespotify:playback and spotify:volume
  • The check runs in services::automations::save_nodes, so saveAutomationNodes and PUT /v1/automations/{id}/nodes refuse with the same 403 FORBIDDEN, Missing permission: <permission>, and write nothing. The MCP lumio_automation_nodes_save tool goes through the same service after its own, stricter save table (it refuses a ban node outright). The table is owner_acting_permissions in apps/api/src/services/mcp/automation_rules.rs, next to the MCP-only rules.
  • A node stored unchanged (same id, type and config) is not checked again. A member without chat:ban can still edit other nodes of an automation the owner built; changing the ban node is checked.
  • Saved nodes keep running whoever saved them; the check applies at save time only.

Execution limits​

The executor (crates/lo-automation/src/executor.rs) enforces these limits on every run, whatever is stored:

LimitValueWhen exceeded
Node entries per run (MAX_STEPS)1,000, trigger nodes reached mid-walk includedThe walk stops: the last step fails with Limit exceeded: automation work budget exhausted … and completed is false
Recursion depth (MAX_DEPTH)100Same as above
Trigger node entriesOnce per run; a path back into a trigger ends there—
Loop iterations (MAX_LOOP_ITERATIONS)100 per Loop nodeThe count is clamped
Delay (MAX_DELAY_MS)1 hour per Delay nodeThe delay is clamped
Rendered template (MAX_TEMPLATE_OUTPUT_BYTES)16 KiB per template (chat message, Discord/overlay message, webhook URL, header or body, condition operand, variable value)The step fails with Limit exceeded: rendered template exceeds 16384 bytes. Chat bots still truncate to the platform's own message limit
Variable value (MAX_VARIABLE_VALUE_BYTES)4 KiBThe step fails; nothing is stored
Variables per run (MAX_VARIABLES_PER_AUTOMATION)100, extension node output includedThe step fails
Stored variable lifetime (VARIABLE_TTL_SECS)30 days, refreshed on every writeThe Redis key expires

A random node stored with min > max (saved before validation existed) uses the swapped range instead of failing. A timer_trigger stored with an interval below 60 s fires every 60 s.

Key Files​

PathDescription
apps/api/src/graphql/automations.rsGraphQL queries and mutations
apps/api/src/db/automations.rsDatabase CRUD for automations, nodes, edges
crates/lo-automation/src/Execution engine and limits (executor.rs), save-time graph validation (validate.rs), template context, node types (types.rs)
apps/api/src/services/automations.rsCreate, update, delete and node/edge save shared by GraphQL and REST: ownership check, restore of redacted webhook credentials, save-time validation, webhook reconcile
apps/api/src/services/automation_secrets.rsWrite-only webhook credentials: redaction on every read, placeholder restore on save (see Credentials in webhook nodes)
apps/api/src/services/automation_graph.rsSave-time validation shared by GraphQL and REST (graph rules + extension install ownership)
apps/api/src/routes/automations.rsREST endpoints
apps/api/src/dispatch.rsRedisActionDispatcher for action execution (action → consumer inventory)
crates/lo-cache/src/bot_action.rsShared bot action envelope and type-name constants
apps/api/src/workers/automation.rsbuild_execution_graph helper

Extension Nodes​

Third-party developers can create custom automation nodes via the Extension Platform. The Automation Builder does not list installed extension nodes in its toolbar yet, so a new extension node cannot be added from the dashboard. An automation that already contains an extension node (for example one saved over the API) shows it on the canvas; selecting it shows a note that extension nodes cannot be configured in the builder yet.

Node types​

TypeDescription
TriggerStarts an automation from an external event (webhook or polling)
ActionPerforms work during automation execution
LogicBranches the flow based on a condition

Extension nodes execute in V8 isolates inside the Automation Worker service. Each node has a 2-5 second timeout depending on type, enforced inside the worker (see the developer docs' Runtime limits).

An extension node names its install in config.extension_meta.install_id. The install must belong to the account that owns the automation: the save paths reject any other install, and the Automation Worker resolves the install by (install_id, account_id, extension_id) at run time, where account_id comes from the automation row. A mismatch fails the step with Install not found. Installs of a suspended extension are dropped from the worker's registry on its next sync (at most about 30 seconds).

Installation​

Extension automation nodes are installed from the Extension Store like any other extension. Uninstalling removes the node from the toolbar and disables any automations that use it (with a warning notification).

Feature flag​

Extension automation nodes require the feature:automation_node_extensions feature flag. This flag gates:

  • Installation of automation_node extensions in the store
  • The "Extensions" section of the Automation Builder toolbar (the toolbar renders it only when the host passes installed nodes, which the dashboard does not do yet)

Webhook triggers​

Extension trigger nodes can receive external webhooks. When an automation with a webhook trigger is enabled:

  1. A webhook URL and secret are generated
  2. The user configures the external service with this URL and the X-Webhook-Secret header
  3. Incoming webhooks are validated and forwarded to the Automation Worker
  4. If the handler returns fired: true, the automation runs

Key files​

PathDescription
apps/automation-worker/Automation Worker HTTP service (V8 handler executor)
crates/lo-automation-worker/Automation Worker library crate
crates/lo-automation/src/executor.rsAutomation Engine with extension node dispatch