Skip to content

Validation System

Validation System

Moira performs comprehensive validation at multiple levels to ensure workflow integrity and correct agent responses.

Validation Levels

1. JSON Schema Validation

Workflows are validated against a JSON Schema definition:

  • Structure validation against workflow schema
  • Required field checking (id, metadata, nodes)
  • Type validation for all properties
  • Built-in node schema branches and the namespaced extension-node form

2. Structural Validation

Graph structure is analyzed for correctness:

  • Node connectivity - All connections point to valid nodes
  • Required nodes - Start node must exist
  • Circular dependencies - Loops are detected and flagged
  • Unreachable nodes - Nodes no entry point leads to. Entry points are the start node and every teleport: a teleport has no ordinary incoming connections, so whatever it routes to is reachable through step({ teleportTo }) and is not reported
  • Registry entries - Each variableRegistry entry must be a valid JSON Schema (a malformed items/pattern/etc. is a blocking error) with a non-empty description
  • Materialize declarations - Every entry has exactly one source (from or empty content), registry sources are string defaults, declared paths are safe and unique, and all connection targets exist
  • Process blocks - When progress is present, every node declares progressNodeId for an existing block, every block has content.summary, every connection leaving a block or returning carries a connectionLabels entry (returns with cycle.cause and cycle.exit), and each {{progress_*_outcome}} template sits on one block that owns a writer; each violation is an error with a stable code (unowned-node, unknown-block, empty-block, empty-description, unlabeled-edge, unexplained-cycle, outcome-duplicate, outcome-unowned, unconnected-block)
  • Extension node types - Types found in the live extension registry or a published registry snapshot validate their declared configuration schema. A live registry that does not contain a type reports an error; a type absent from a snapshot, or validation without usable registry data, produces an unresolved warning instead

3. Input Validation

Agent responses are validated against inputSchema:

  • AJV-based JSON Schema validation
  • Type checking for response fields
  • Required field validation
  • Pattern and format validation

Validation Results

Validation returns structured results:

{
valid: boolean;
errors: ValidationError[];
warnings: ValidationWarning[];
}

Error Types

TypeDescriptionExample
schemaJSON structure invalidMissing required field
structureGraph structure invalidOrphan node
connectionConnection invalidPoints to non-existent node
referenceReference invalidInvalid subgraph ID

Warning Types

TypeDescriptionThreshold
performanceLarge workflow>20 agent-directive nodes
complexityComplex conditionsDeeply nested conditions
contextLarge context>100KB context size

Validation Examples

Valid Workflow

{
"id": "valid-workflow",
"metadata": {
"name": "Valid Workflow",
"version": "1.0.0",
"description": "A valid workflow"
},
"nodes": [
{ "id": "start", "type": "start", "connections": { "default": "task" } },
{
"id": "task",
"type": "agent-directive",
"directive": "...",
"completionCondition": "...",
"connections": { "success": "end" }
},
{ "id": "end", "type": "end" }
]
}

Result:

{ "valid": true, "errors": [], "warnings": [] }

Invalid Workflow - Missing Connection

{
"nodes": [
{ "id": "start", "type": "start", "connections": { "default": "missing" } },
{ "id": "end", "type": "end" }
]
}

Result:

{
"valid": false,
"errors": [
{
"type": "connection",
"message": "Node 'start' references non-existent node 'missing'",
"nodeId": "start"
}
]
}

Workflow with Warning

Large workflow triggers performance warning:

{
"valid": true,
"errors": [],
"warnings": [
{
"type": "performance",
"message": "Workflow has 25 agent-directive nodes. Consider breaking into subgraphs.",
"count": 25
}
]
}

Input Schema Validation

Agent responses are validated against inputSchema defined on agent-directive nodes.

Nodes Without inputSchema

Nodes without inputSchema require empty input from agent. Non-empty responses are rejected:

// Node without inputSchema
{ "id": "task", "type": "agent-directive", "directive": "..." }
// Valid: empty response
{}
// Invalid: non-empty response
{ "result": "done" } // Rejected with validation error

Schema Definition

{
"type": "agent-directive",
"inputSchema": {
"type": "object",
"properties": {
"result": { "type": "string" },
"confidence": { "type": "number", "minimum": 0, "maximum": 10 }
},
"required": ["result"]
}
}

Valid Response

{ "result": "completed", "confidence": 8 }

Invalid Response

{ "confidence": "high" }

Error:

{
"valid": false,
"errors": [
{ "field": "result", "message": "Required field missing" },
{ "field": "confidence", "message": "Expected number, got string" }
]
}

Declared-But-No-Default Variable Warning

The validator emits a warning (severity warning, not an error — the workflow is still valid) when a variableRegistry variable is referenced in a directive, completionCondition, message, or condition, but the variable has no default and is never written by any upstream node’s globalInputs (and is not present in the start node’s initialData).

At runtime, such a reference renders the literal placeholder [[UNDEFINED_VARIABLE]] instead of a value.

{
"valid": true,
"errors": [],
"warnings": [
{
"type": "structure",
"severity": "warning",
"nodeId": "do-work",
"message": "Variable 'iteration' is referenced in node 'do-work' but has no default and is never written by an upstream node. It will render [[UNDEFINED_VARIABLE]] at runtime."
}
]
}

Fix it one of two ways:

  • Add a default to the variable in variableRegistry.
  • Have an upstream node write the variable via its globalInputs before the node that references it.

Playbook Reference Validation

A definition that names a playbook with {{playbook:name}} is accepted only when the author can read that playbook: their own, or another account’s published one. An unreadable reference is a blocking error wherever a definition is written — the manage tool refuses to create or edit, and a definition saved through the API is stored as invalid — and the message names the playbook and the three ways out: create it, publish it, or remove the reference.

The same rule runs once more when a run is started, because a playbook can disappear between the last edit and the start. start refuses before the execution exists rather than presenting a step whose behaviour text is missing.

A reference written with a leading backslash (\{{playbook:name}}) is literal text and is not checked — that is how a document can explain the syntax without naming a playbook.

Injection Safety

Substituted variable and data VALUES are never re-executed as templates. When the engine interpolates a value into a directive or message, that value is treated as a literal string — brace syntax originating from substituted data is neutralized and not re-parsed.

This means templates only ever execute in author-controlled static node fields, not in values that arrive from agent input or external data.

Playbook content follows the same boundary. Your own playbook is author-controlled text and is processed like the rest of your definition; a playbook belonging to another account is neutralized before substitution, so published text cannot execute templates inside your run.

Do not echo untrusted input containing {{...}} into a directive. Declare the variables you need and reference them explicitly. Prefer explicit named variables over {{context.variables}} full-dumps, which can expose the entire variable bag.

Materialize Validation

A materialize node is accepted only when basePath is non-empty, files contains 1–100 entries, and connections.success is present. Every declared files[].path must be a normalized relative path without NUL, absolute roots, backslash roots, empty segments, ., or ..; declared paths must be unique. Each entry declares exactly one of:

  • from: the name of a variableRegistry entry with type: "string" and a string default;
  • content: "": an empty skeleton file. Non-empty inline content is rejected.

Because paths can contain templates, the same safety and collision checks run again after rendering against the current execution context. Runtime rendering additionally enforces 1 MiB per file and 10 MiB total uncompressed content. A failure while preparing the step routes through the optional connections.error; otherwise it is raised. Every HTTP download revalidates the five-minute grant and its user/execution/node binding; repeats are accepted only while the bound execution remains waiting on that node. Context delivery revalidates the same bindings and adds one further bound: a set over 256 KiB in total is refused rather than truncated.

Best Practices

  1. Always include inputSchema - Validate agent responses for consistent data
  2. Keep workflows focused - Split large workflows into subgraphs
  3. Test validation - Use manage with includeValidation: true
  4. Handle errors gracefully - Define error connections for validation failures