Automation interface
@antelopejs/interface-dms-automation is the consumer contract for backend modules that add nodes to the builder. Import from its root; the internal proxy namespace is not a supported consumer entry point.
Register a complete action
Start with a trace marker. It records that a step ran without logging its input or calling an external service. Wire start() and stop() into your module's lifecycle; the builder exposes the registered action and its recorded output.
import { RegisterActionType, UnregisterActionType } from "@antelopejs/interface-dms-automation";
const ACTION_ID = "example.trace-marker";
export function start(): void {
RegisterActionType({
id: ACTION_ID,
name: "Record trace marker",
description: "Record that this step was reached",
icon: "i-lucide-file-text",
inputSchema: { type: "object" },
outputSchema: { type: "object", properties: { recorded: { type: "boolean" } } },
async execute(_input, ctx) {
ctx.log("info", "Trace marker reached");
return { recorded: true };
},
});
}
export function stop(): void {
UnregisterActionType(ACTION_ID);
}
For dependency setup, see Extending. Before replacing the action body with a real operation, read the execution context and authorization constraints.
Registration and lifecycle
import type { ActionType, DataNodeType, TriggerType } from "@antelopejs/interface-dms-automation";
declare function RegisterTriggerType(type: TriggerType): void;
declare function RegisterActionType(type: ActionType): void;
declare function RegisterDataNodeType(type: DataNodeType): void;
declare function UnregisterTriggerType(id: string): void;
declare function UnregisterActionType(id: string): void;
declare function UnregisterDataNodeType(id: string): void;
Register in the declaring module's start() and unregister in stop(). Registrations queue before the implementation attaches, replay on attachment/reload, and belong to the calling module. With no implementation, registering does not execute nodes or throw an availability error; it retains the declaration for a later attachment. The unregister functions remove declarations, not saved procedure graphs or run history. Removing a trigger calls deactivate on its active handles before the unregister call returns, while the declaring module can still run code: when a module stops or reloads, the core takes its registrations back and invalidates its context right after. The call does not await cleanup.
Each active handle retains the trigger definition that created it. A reload during an asynchronous activation cleans up each handle with its own definition before reconciling the replacement, even if the registry has already changed. A handle whose module is already gone counts as torn down without an error, and nothing it emits afterwards starts a run.
Runs started by emit belong to the Automation module, whichever module calls it, so a stopping trigger module does not take its runs down with it. When the module behind a step goes away during a run (the step throws ModuleContextInvalidatedError, or its type is unregistered after the run started), the run ends as cancelled, not failed: Try/Catch and Retry do not catch it, no failed step is recorded, and the health figures leave it out. A type already missing when the run starts still fails it.
IDs are registry keys. Namespace them as <module>.<node>: a collision replaces the existing definition instead of creating a second node. Every definition requires id, name, description and icon strings. JsonSchema is Record<string, unknown>; keep schemas flat because the builder maps properties to ports. A schema describes editor inputs, not a tenant authorization policy.
TriggerType
TriggerType<TConfig = unknown, TOutput = unknown> adds these required members:
interface TriggerMembers<TConfig, TOutput> {
configSchema: Record<string, unknown>;
outputSchema: Record<string, unknown>;
cluster: "singleton" | "replicated";
activate(config: TConfig, emit: (payload: TOutput) => void): Promise<unknown>;
deactivate(handle: unknown): Promise<void>;
}
activate installs the trigger and returns an opaque handle that deactivate must clean up. emit starts a run; it returns void, not a completion promise. Release timers, listeners and routes during deactivation. Activation/deactivation failures are runtime arming failures, not registration return values.
Choose cluster explicitly. singleton activates only on the elected leader, for schedules or feeds that must not fire on every instance. replicated activates on every instance, for inputs such as webhooks that a load balancer routes to one instance. The distinction is inert in single-instance mode. It is not an exactly-once guarantee for arbitrary action side effects; design externally visible actions for retries. See cluster configuration.
ActionType and ExecutionCtx
ActionType<TInput = unknown, TOutput = unknown> adds inputSchema, outputSchema, and execute(input: TInput, ctx: ExecutionCtx): Promise<TOutput>. Actions are asynchronous and may perform I/O. Returning a value makes it available to downstream ports; throwing records a failed execution.
ExecutionCtx contains procedureId, runId and nodeId strings, plus log(level: LogLevel, message: string, value?: unknown): void. LogLevel is "info" | "warn" | "error". The runtime tags entries with the node and records them in the run trace. Do not put secrets in logs or assume the context contains a tenant identity: it has no tenantId or authenticated user. Validate and authorize tenant-scoped operations in your action.
Procedures belong to the platform, not to a tenant, and many triggers (a schedule, a webhook) have no tenant at all, so the context cannot carry one. An action that works on tenant data takes the tenant as an input, as the DMS's own actions do: declare a tenantId string in inputSchema, wire it from the tenantId output of a DMS event trigger (or type it in), and fall back to DEFAULT_TENANT_ID from @antelopejs/interface-dms when it is empty.
DataNodeType
DataNodeType<TInput = unknown, TOutput = unknown> adds category: string, inputSchema, outputSchema, optional configSchema, and synchronous evaluate(input: TInput): TOutput. Keep evaluation pure; use an action for asynchronous work or side effects. Evaluation errors fail the dependent execution.
When supplied, configSchema replaces the inspector's editable fields, not the data-input ports: ports still derive from inputSchema. The runtime merges the configured values into the evaluation input. Data nodes receive no execution context.
List types and invoke procedures
import type {
ActionType,
DataNodeType,
ProcedureRunOutcome,
RunProcedureOptions,
TriggerType,
} from "@antelopejs/interface-dms-automation";
declare function ListTriggerTypes(): Promise<TriggerType[]>;
declare function ListActionTypes(): Promise<ActionType[]>;
declare function ListDataNodeTypes(): Promise<DataNodeType[]>;
declare function InvokeProcedure(id: string, payload?: unknown): Promise<string>;
declare function RunProcedure(id: string, options?: RunProcedureOptions): Promise<ProcedureRunOutcome>;
All five interface calls are asynchronous. Listing returns the currently registered definitions. Calls reject if no implementation is loaded; catch failures when Automation is optional. Registrations can be optional dependencies, but invocations still require a running implementation.
InvokeProcedure runs the saved graph from its first manual trigger and resolves to the stored run ID after execution and recording. It rejects with 404 for an unknown procedure and 400 for an unparseable stored graph or a graph without a manual trigger. A recorded failed execution can still return a run ID: the ID is not a success flag. Storage failures may reject after actions have run. There is no rollback or idempotency key on this call.
RunProcedure starts any saved procedure now, whatever its triggers (a schedule, a webhook, a manual trigger or one a module registers), the way Run now does in the builder. It waits until the Automation module has started.
| Option | Type | Description |
|---|---|---|
payload | unknown | What the trigger hands the graph. Left out, the last payload that trigger received in a stored run, or {} when none was kept. Pass it explicitly for a reproducible run. |
triggerNodeId | string | The trigger node to start from. By default the manual trigger, else the graph's first trigger. |
It resolves once the run is stored, to a ProcedureRunOutcome: { runId, status, errorMessage? }, where status is "ok", "failed" or "cancelled" (a module one of its steps belongs to went away while it ran). It rejects, as the HTTP run-now route does, with 404 for an unknown procedure or trigger node, and 400 for an unparseable stored graph or a graph with blocking issues (no trigger, a step missing its settings…), whose issues the rejection lists. Like Run now, it runs a paused procedure too: pausing stops its triggers, not explicit runs. The run is stored as an ordinary run, counted in the health figures.
This is a trusted, platform-level backend API, not a tenant-scoped HTTP route. The caller must restrict which procedures can run and what payloads they receive. Do not expose arbitrary procedure IDs to untrusted users.
Declare procedures from code
import type { DeclaredProcedure, ProcedureDeclaration } from "@antelopejs/interface-dms-automation";
declare function DeclareProcedure(declaration: ProcedureDeclaration): Promise<DeclaredProcedure>;
DeclareProcedure creates or refreshes a procedure your module ships as code, such as the procedures a project template or a demo provides. Call it on every boot: it is idempotent and keyed by declaration.id.
| Field | Type | Description |
|---|---|---|
id | string | Stable key you choose, namespaced <module>.<procedure>: letters, digits, ., _ and -, at most 120 characters. It becomes the procedure's ID, so InvokeProcedure(id), RunProcedure(id) and the builder (/modules/automation/builder?selected=<id>) take it as is. |
name | string | 1 to 200 characters. |
description | string | Optional, at most 2000 characters. |
graph | ProcedureGraph | The graph, in the shape "Export JSON" writes under graph: nodes, triggerEdges, dataEdges. Design it in the builder and export it, or write it by hand. |
enabled | boolean | Required. After storage, the module reconciles eligible triggers locally and notifies peers in Redis mode. Singleton triggers run only on the leader; replicated triggers run on each instance. |
The declaration is validated as Import JSON validates a file, and, when it is enabled, as enabling the procedure in the builder is: a missing setting, a missing trigger, an unknown template or a webhook route another enabled procedure answers on rejects the call with an Error naming each problem, and nothing is stored. A disabled declaration is stored as a draft with its problems. One exception: a trigger, action or data node type that is not registered yet does not block, because modules start in parallel; the trigger arms as soon as its type registers.
What happens to an existing procedure follows one rule. The module keeps a hash of the name, description, graph and enabled flag you declared last:
| Stored procedure | status | Effect |
|---|---|---|
| None with this ID | "created" | Stored, version 1. |
| Identical to the last declaration, and so is this one | "unchanged" | Declared content, revision, version and modification time stay unchanged. Run-history metadata and trigger subscriptions are synchronized again. |
| Identical to the last declaration, this one differs | "updated" | Replaced. The version moves on when the graph changes what a run does, and a builder still open on the old revision cannot save over it. |
| Changed since the last declaration | "kept" | Left as it is. |
Any change counts: an edit saved in the builder, a pause or a resume, and a procedure the module disabled because a template it uses broke. A procedure brought back to exactly the declared content (paused, then resumed) follows your declarations again. A deleted procedure is created again by the next call: pause it instead to keep it out of the way. The result also carries enabled, the stored procedure's state (a kept one may differ from your declaration), and issues, the graph's problems that did not block it.
Storage and synchronization are not one transaction. A database or notification failure can reject the call after the procedure is stored. Retry the same declaration: an unchanged result also retries run-history synchronization and trigger reconciliation, without advancing the procedure's revision or version. This also applies when a concurrent caller has already stored the same declaration. A kept result does not overwrite the user's content.
enabled: true does not guarantee a healthy trigger or a completed run. Trigger activation failures remain visible as arming errors in the dashboard; issues describes the submitted graph, not those runtime failures. The call waits for local reconciliation, not acknowledgments from every peer. Retry-safe storage does not make the actions a trigger fires exactly-once.
The call waits until the automation module has started, so it is safe from your start() or a Hook.DATABASE_INITIALIZED handler. Without an implementation loaded, it waits until one attaches: do not await it in construct(), which runs before any module starts.
import { DeclareProcedure, type ProcedureGraph } from "@antelopejs/interface-dms-automation";
const graph: ProcedureGraph = {
nodes: [
{
id: "weekday-morning",
kind: "trigger",
typeId: "schedule.cron",
config: { cron: "0 9 * * 1-5" },
position: { x: 80, y: 160 },
},
{
id: "note",
kind: "action",
typeId: "log.message",
config: { message: "Follow up the open quotes" },
position: { x: 360, y: 160 },
},
],
triggerEdges: [{ id: "edge-1", from: { node: "weekday-morning" }, to: { node: "note" } }],
dataEdges: [],
};
export async function start(): Promise<void> {
const declared = await DeclareProcedure({
id: "example.quote-follow-up",
name: "Follow up open quotes",
description: "Every weekday morning, list the quotes waiting for an answer",
graph,
enabled: true,
});
if (declared.status === "kept") {
console.info(`${declared.id} was changed in the builder: left as it is`);
}
}