Skip to content

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"
}
}
PropertyRequiredDescription
idYesConvention: should be “start”
typeYesMust be "start"
connections.defaultYesNext 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"]
}
PropertyRequiredDescription
idYesConvention: should be “end”
typeYesMust be "end"
finalOutputNoContext 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"
}
}
PropertyRequiredDescription
directiveYesWhat the agent should do
completionConditionYesWhen the step is complete
inputSchemaNoJSON Schema for response validation
inputSchema.globalInputsNoNames of variableRegistry globals this node writes
inputSchema.propertiesNoNode-local outputs (referenced as node-id.name)
connections.successYesNext 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"
}
}
PropertyRequiredDescription
conditionYesStructured condition object
connections.trueYesNext node when condition is true
connections.falseYesNext 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, neq
  • gt, gte, lt, lte
  • contains
  • exists
  • and, 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"
}
}
PropertyRequiredDescription
expressionsYesArray of assignment expressions
connections.defaultYesNext node after successful evaluation
connections.errorNoNext 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"
}
}
PropertyRequiredDescription
graphIdYesReferenced workflow ID
inputMappingYesParent context -> subgraph context
outputMappingYesSubgraph context -> parent context
connections.successYesNext 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"
}
}
PropertyRequiredDescription
messageYesNotification text with template support
formatNoPortable format: plain, markdown, or html
silentNoRequest silent delivery where the provider supports it
attachProgressImageNoAttach the current bounded workflow-progress PNG
attachmentNoOne bounded base64 image or document with name and MIME
connections.defaultYesFull, partial, or no-eligible-channel continuation
connections.errorNoTotal 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" }
}
PropertyRequiredDescription
hintYesHuman-readable description of when to use teleport
directiveYesInstruction shown to agent after teleport
completionConditionYesSuccess criteria for the teleport step
inputSchemaNoJSON Schema for agent response validation
connections.successYesNext node after teleport input provided
connections.errorNoError 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 input when 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"
}
}
PropertyRequiredDescription
reasonYesLock reason (supports {{variable}} templates)
connections.unlockedYesNext node after lock is unlocked

Behavior:

  1. Starting any workflow that contains a lock node requires a valid Telegram bot token and chat ID for the current user. Neither skipNotificationCheck nor its deprecated skipTelegramCheck alias can bypass this requirement.
  2. 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.
  3. Missing or invalid settings and delivery failures create no usable active lock or context reference. Revisiting the node retries with a fresh delivery attempt.
  4. Subsequent visits check the active lock or validate a user-supplied PIN, then route through connections.unlocked after 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"
}
}
PropertyRequiredDescription
basePathYesRendered destination must be non-empty and contain no NUL character
filesYes1–100 archive entries
files[].pathYesTemplated safe path relative to basePath
files[].fromOne ofvariableRegistry string entry whose current default is the source
files[].contentOne ofMust be exactly ""; creates a skeleton file
connections.successYesNext node after empty completion input
connections.errorNoRoute 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:

Terminal window
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"
}
}
PropertyRequiredDescription
outputVariableYesContext variable to store results
filter.tagNoFilter by exact tag
filter.keyPatternNoFilter by key prefix
filter.keySearchNoSearch in key (contains)
singleModeNoReturn object instead of array
connections.errorNoError 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"
}
}
PropertyRequiredDescription
keyNo*Note key (required in single mode)
sourceYesContext variable with value
tagsNoTags to assign
batchModeNoProcess 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"
}
}
PropertyRequiredDescription
search.tagNoSearch by tag
search.keyPatternNoSearch by key prefix
keyTemplateYesKey for new note if not found
valueYesContext variable with note value

All filter and key parameters support {{ variable }} template expressions.

Best Practices

  1. Start with start - Every workflow needs exactly one start node
  2. End with end - Use end nodes to mark completion points
  3. Clear Directives - Be specific about what the agent should do
  4. Verifiable Conditions - Completion conditions should be objectively measurable
  5. Schema Validation - Use inputSchema for structured responses
  6. Error Paths - Use an error connection only where that node type’s documented runtime behavior routes it