Nodes
Nodes are the building blocks of Moira workflows. Each node represents a step in the process with specific behavior defined by its type.
Node Types
The list below is Moira’s built-in set. An installation may also expose namespaced node types such
as extension-name.node-name from installed extensions. The live extension registry supplies their
configuration schemas to validation, while Moira’s node-type catalog describes them to the workflow
viewer.
Start
Entry point for workflow execution.
End
Terminal node marking workflow completion.
Agent Directive
Agent task with directive and completion condition.
Condition
Branch execution based on structured conditions.
Expression
Compute values using arithmetic expressions.
Subgraph
Delegate to another workflow.
User Notification
Send a notification through every enabled communication channel configured by the current user.
Telegram Notification (deprecated)
Telegram-only compatibility node for existing provider-specific workflows.
Teleport
Jump target reachable only via explicit teleport.
Lock
PIN-based execution gate with Telegram approval.
Materialize
Deliver registry-backed files through a five-minute, node-bound tar grant.
Start Node
Entry point for workflow execution. Every workflow must have exactly one start node.
{ "id": "start", "type": "start", "connections": { "default": "first-task" }}| Property | Required | Description |
|---|---|---|
id | Yes | Convention: should be “start” |
type | Yes | Must be "start" |
connections.default | Yes | Next node ID |
The start node seeds the workflow’s global variables from the variableRegistry default values. Declare globals in the workflow-level variableRegistry, not on the start node.
End Node
Terminal node marking workflow completion. No outgoing connections.
{ "id": "end", "type": "end", "finalOutput": ["result", "summary"]}| Property | Required | Description |
|---|---|---|
id | Yes | Convention: should be “end” |
type | Yes | Must be "end" |
finalOutput | No | Context keys to include in final result |
note: End nodes have no connections - they are terminal nodes.
Agent Directive Node
The primary node type for agent tasks. Contains a directive (what to do) and completion condition (when done).
{ "id": "analyze-requirements", "type": "agent-directive", "directive": "Analyze the requirements document and identify key features", "completionCondition": "Features are listed with priorities", "inputSchema": { "type": "object", "globalInputs": ["analysis_done"], "properties": { "features": { "type": "array", "items": { "type": "string" } } }, "required": ["analysis_done", "features"] }, "connections": { "success": "next-step" }}| Property | Required | Description |
|---|---|---|
directive | Yes | What the agent should do |
completionCondition | Yes | When the step is complete |
inputSchema | No | JSON Schema for response validation |
inputSchema.globalInputs | No | Names of variableRegistry globals this node writes |
inputSchema.properties | No | Node-local outputs (referenced as node-id.name) |
connections.success | Yes | Next node after valid input |
In the example, analysis_done is a declared global (it must exist in variableRegistry) written by this node and readable elsewhere as {{analysis_done}}; features is a node-local output readable as {{analyze-requirements.features}}. A returned key that is neither a declared global nor a described local output is rejected.
Use inputSchema to ensure structured responses. globalInputs lists the globals the node
writes; properties describes its node-local outputs. The engine validates and routes the
response automatically.
If a submission does not match inputSchema, the engine logs the rejection and pauses again at
the same node. The corrective message describes the expected schema and errors without echoing the
rejected payload. Per-node retry fields found in older definitions are compatibility-only and do
not bound or redirect this validation cycle; model business retry and escalation explicitly in the
workflow graph.
Condition Node
Branch execution based on structured conditions:
{ "id": "check-result", "type": "condition", "condition": { "operator": "eq", "left": { "contextPath": "status" }, "right": "success" }, "connections": { "true": "success-path", "false": "retry-step" }}| Property | Required | Description |
|---|---|---|
condition | Yes | Structured condition object |
connections.true | Yes | Next node when condition is true |
connections.false | Yes | Next node when condition is false |
Structured Conditions
Conditions use a structured format (not string evaluation):
{ "operator": "and", "conditions": [ { "operator": "gt", "left": { "contextPath": "score" }, "right": 80 }, { "operator": "eq", "left": { "contextPath": "validated" }, "right": true } ]}Supported operators:
eq,neqgt,gte,lt,ltecontainsexistsand,or,not
Expression Node
Compute values using arithmetic expressions. Useful for counters, calculations, and variable transformations:
{ "id": "increment-counter", "type": "expression", "expressions": ["counter = counter + 1", "result = counter * multiplier"], "connections": { "default": "next-step", "error": "error-handler" }}| Property | Required | Description |
|---|---|---|
expressions | Yes | Array of assignment expressions |
connections.default | Yes | Next node after successful evaluation |
connections.error | No | Next node on evaluation error |
Expression Syntax
Expressions support basic arithmetic operations:
- Arithmetic:
+,-,*,/ - Parentheses:
(a + b) * c - Assignment:
result = a + b - Context paths:
step.index,plan.items[0].value,tasks[current_index].action
{ "expressions": ["total = price * quantity", "tax = total * 0.1", "final_price = total + tax"]}Expression evaluation uses a custom sandboxed parser, NOT JavaScript eval. Member reads use own properties only; fixed or variable array indexes must resolve to in-bounds non-negative integers. Assignment targets are safe bare variable names. With a variable registry, assignments must name declared globals and satisfy their JSON Schemas; the node publishes nothing on error.
Error Handling
Expression nodes can fail in two cases:
- Division by zero
- Invalid, unsafe, unresolved, or out-of-bounds member read
- Undeclared assignment target or value outside its registry schema
When an error occurs, execution routes to the error connection if defined, otherwise the workflow fails.
Subgraph Node
Delegate execution to another workflow:
{ "id": "run-tests", "type": "subgraph", "graphId": "test-workflow", "inputMapping": { "codeDir": "projectPath" }, "outputMapping": { "testResults": "results" }, "connections": { "success": "next-step" }}| Property | Required | Description |
|---|---|---|
graphId | Yes | Referenced workflow ID |
inputMapping | Yes | Parent context -> subgraph context |
outputMapping | Yes | Subgraph context -> parent context |
connections.success | Yes | Next node on success |
Subgraphs enable workflow composition and reuse. The agent sees it as a continuous workflow.
connections.error is optional and is not the route for thrown child-execution or mapping
failures in the current runtime. Those failures are logged and execution pauses at the subgraph so
the agent can correct the cause and retry.
User Notification Node
Send through every valid enabled channel configured by the execution user. The workflow cannot choose a provider, recipient, or credential:
{ "id": "notify-complete", "type": "user-notification", "message": "Workflow {{workflowName}} completed successfully", "format": "markdown", "silent": false, "connections": { "default": "next-step", "error": "notification-failed" }}| Property | Required | Description |
|---|---|---|
message | Yes | Notification text with template support |
format | No | Portable format: plain, markdown, or html |
silent | No | Request silent delivery where the provider supports it |
attachProgressImage | No | Attach the current bounded workflow-progress PNG |
attachment | No | One bounded base64 image or document with name and MIME |
connections.default | Yes | Full, partial, or no-eligible-channel continuation |
connections.error | No | Total attempted failure; otherwise it also uses default |
attachment and an enabled attachProgressImage are mutually exclusive. Attachment filenames
use the safe portable allowlist and MIME types must have a valid type/subtype form. The service
applies shared per-user/provider rate limits, per-user and provider concurrency, deadlines, text
and byte limits before provider delivery. A configured Telegram channel supports text, PNG/JPEG
images, and documents.
The node result is stored under its node ID. userNotificationStatus is delivered, partial,
no_configured_channels, or all_failed; configuredChannels, deliveredChannels, and
channels contain counts and sanitized per-channel status/reason values. A configured channel that
cannot carry the requested attachment is reported as unsupported and skipped; without another
eligible channel or availability failure, the aggregate is no_configured_channels, not
all_failed. Results and errors do not contain credentials, recipients, message bodies, or
attachment bytes.
Deprecated Telegram-specific node
telegram-notification remains executable for existing workflows. It sends only through Telegram,
and an authored chatId keeps its exact provider-specific recipient meaning. New ordinary
notifications should use user-notification; changing an explicit legacy recipient to generic
fan-out requires an intentional workflow migration.
Teleport Node
Jump target reachable only via explicit teleport, not via normal connections. Behaves like agent-directive (pauses for input, validates schema) but is only reachable when the agent explicitly requests a teleport jump.
{ "id": "teleport-replan", "type": "teleport", "directive": "Rewrite the development plan", "completionCondition": "New plan created and validated", "hint": "Use when current plan needs restructuring", "inputSchema": { "type": "object", "properties": { "reason": { "type": "string" } }, "required": ["reason"] }, "connections": { "success": "plan-node" }}| Property | Required | Description |
|---|---|---|
hint | Yes | Human-readable description of when to use teleport |
directive | Yes | Instruction shown to agent after teleport |
completionCondition | Yes | Success criteria for the teleport step |
inputSchema | No | JSON Schema for agent response validation |
connections.success | Yes | Next node after teleport input provided |
connections.error | No | Error handler node |
Teleport nodes must NOT have incoming connections from other nodes. They are excluded from unreachable node warnings during validation.
Using Teleport at Runtime
When a workflow contains teleport nodes, their hints are automatically appended to each step response under “Available Teleport Jumps”. To jump to a teleport node, use the teleportTo parameter in step():
step({ processId: "abc123", attemptId: "attempt-current", teleportTo: "teleport-replan" })- Only teleport-type nodes can be targets
- Use the Step attempt ID from the current presentation
- Do NOT provide
inputwhen teleporting — the teleport node will present its own directive - Execution context (all variables) is preserved across the teleport
- After providing input to the teleport node, execution continues via its
connections.success
Lock Node
PIN-based execution gate. It delivers the PIN to the current user’s configured Telegram chat with an Approve inline keyboard and pauses only after that delivery succeeds.
{ "type": "lock", "id": "approval-gate", "reason": "Deploy to production for {{workflow_name}}", "connections": { "unlocked": "proceed-node" }}| Property | Required | Description |
|---|---|---|
reason | Yes | Lock reason (supports {{variable}} templates) |
connections.unlocked | Yes | Next node after lock is unlocked |
Behavior:
- Starting any workflow that contains a lock node requires a valid Telegram bot token and chat ID for the current user. Neither
skipNotificationChecknor its deprecatedskipTelegramCheckalias can bypass this requirement. - On the first visit, Moira stores only a hashed pending PIN, sends the plaintext PIN to that configured chat, activates the lock, stores
_lockId, and pauses the execution. - Missing or invalid settings and delivery failures create no usable active lock or context reference. Revisiting the node retries with a fresh delivery attempt.
- Subsequent visits check the active lock or validate a user-supplied PIN, then route through
connections.unlockedafter resolution.
MCP and workflow responses never contain the generated PIN. The user can enter a PIN supplied out of band or approve the lock from Telegram.
Materialize Node
Delivers author-defined files to the agent filesystem, keeping their rendered contents out of the
agent context. The server creates a five-minute, node-bound tar grant; the agent runs or retries the
exact command in the generated directive and then completes the step with null or {}. A host that
cannot run that command uses the directive’s context-delivery fallback instead, which spends context
and completes the step the same way.
{ "id": "materialize-standards", "type": "materialize", "basePath": "{{workspace_path}}", "files": [ { "path": "standards/planning.md", "from": "planning_standards" }, { "path": "plans/.keep", "content": "" } ], "connections": { "success": "create-plan", "error": "materialize-failed" }}| Property | Required | Description |
|---|---|---|
basePath | Yes | Rendered destination must be non-empty and contain no NUL character |
files | Yes | 1–100 archive entries |
files[].path | Yes | Templated safe path relative to basePath |
files[].from | One of | variableRegistry string entry whose current default is the source |
files[].content | One of | Must be exactly ""; creates a skeleton file |
connections.success | Yes | Next node after empty completion input |
connections.error | No | Route for validation, configuration, or grant-issuance failure |
from and content are mutually exclusive. A from entry must name a declared string variable
with a string default; arbitrary inline text is rejected. When the step is presented, the server
renders basePath and the file-path summary shown in the directive. The rendered basePath is part
of the issued command. When the HTTP archive is requested, the server reloads the current workflow
and re-renders each archive entry path and registry-backed content with the bound execution context.
Changing the workflow after the command was issued can therefore change the downloaded paths or
contents, but not that command’s destination. Re-presenting the paused step uses the current
definition to issue a new command and grant instead of using a seeded execution snapshot.
The generated POSIX command has this shape, with every argument already shell-quoted:
mkdir -p -- '<basePath>' && curl -sSf -- '<reusable-url>' | tar -x -C '<basePath>'Copy the emitted command exactly. Archive entries are relative to basePath; the tar never
contains basePath itself. Paths must be normalized, relative, non-empty, unique after template
rendering, and contain no empty, ., .., absolute, backslash-rooted, or NUL segments. Limits are
100 files, 1 MiB UTF-8 content per file, and 10 MiB total uncompressed content.
The URL expires after five minutes and is bound to the current user, execution, and node. It can be
downloaded repeatedly while the execution remains waiting on that node and is invalid immediately
after the execution advances. The generated directive explains this retry window and reminds the
agent that delivery does not prove reading; each later consumer must still explicitly require the
files it uses to be read. Calling session({ action: "current_step" }) while paused issues a fresh
URL without advancing the graph. There is intentionally no textual content fallback.
See Materialize Files for the complete archive, path, grant, error, and Workflow Management Flow contracts.
Extension nodes
Installed extensions contribute namespaced types such as corporate-messenger.send. Their
manifest supplies the configuration form and validation schemas; the browser renders those schemas
with its generic editor and never loads extension frontend code. A successful result is stored under
the node ID. Input, handler, timeout, runner and output failures follow connections.error when it
exists; otherwise Moira records the diagnostic and pauses on the node for retry.
The live node-type catalogue distinguishes an unavailable extension runner from a live registry in which one extension is not installed. See Writing an Extension for the manifest, SDK and execution contracts.
Input Schema
Define expected response structure using JSON Schema:
{ "inputSchema": { "type": "object", "properties": { "summary": { "type": "string", "description": "Brief summary of findings" }, "items": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "priority": { "type": "number" } } } } }, "required": ["summary"] }}Automatic Node Types
Automatic nodes execute without agent interaction. They run server-side and immediately continue to the next node. Used for note storage operations.
Read Note Node
Reads notes matching filter criteria into context variable:
{ "type": "read-note", "id": "load-notes", "outputVariable": "projectNotes", "filter": { "tag": "{{projectTag}}", "keyPattern": "project-" }, "singleMode": false, "connections": { "default": "next-node", "error": "error-handler" }}| Property | Required | Description |
|---|---|---|
outputVariable | Yes | Context variable to store results |
filter.tag | No | Filter by exact tag |
filter.keyPattern | No | Filter by key prefix |
filter.keySearch | No | Search in key (contains) |
singleMode | No | Return object instead of array |
connections.error | No | Error handler node |
Write Note Node
Writes data from context to notes:
{ "type": "write-note", "id": "save-results", "key": "results-{{timestamp}}", "source": "analysisResults", "tags": ["analysis"], "connections": { "default": "next-node" }}| Property | Required | Description |
|---|---|---|
key | No* | Note key (required in single mode) |
source | Yes | Context variable with value |
tags | No | Tags to assign |
batchMode | No | Process array of notes |
Upsert Note Node
Find-or-create operation:
{ "type": "upsert-note", "id": "upsert-config", "search": { "tag": "config" }, "keyTemplate": "{{projectId}}-config", "value": "configData", "connections": { "default": "next-node" }}| Property | Required | Description |
|---|---|---|
search.tag | No | Search by tag |
search.keyPattern | No | Search by key prefix |
keyTemplate | Yes | Key for new note if not found |
value | Yes | Context variable with note value |
All filter and key parameters support {{ variable }} template expressions.
Best Practices
- Start with start - Every workflow needs exactly one start node
- End with end - Use end nodes to mark completion points
- Clear Directives - Be specific about what the agent should do
- Verifiable Conditions - Completion conditions should be objectively measurable
- Schema Validation - Use
inputSchemafor structured responses - Error Paths - Use an error connection only where that node type’s documented runtime behavior routes it