Validation & Linting

61 diagnostic codes — 10 structural errors and 51 semantic warnings — to catch problems before runtime.

Overview

Dippin provides two levels of analysis:

Structural validation (DIP001-DIP010): Errors that must be fixed. A workflow with any of these cannot execute. Run with dippin validate.

Semantic linting (DIP101-DIP151): Warnings that flag likely bugs or questionable patterns. They don’t block execution but should be reviewed. Run with dippin lint for both levels.

Diagnostic Format

Diagnostics are displayed in a rustc-inspired format:

error[DIP003]: unknown node reference "InterpretX" in edge
  --> pipeline.dip:45:5
  = help: did you mean "Interpret"?

Structural Errors (DIP001-DIP010)

These must be fixed for a workflow to be valid. Each causes exit code 1.

DIP001 — Start Node Missing

The workflow must declare a start: field pointing to an existing node.

error[DIP001]: start node does not exist
  --> pipeline.dip:1:1
  = help: add "start: <NodeID>" to the workflow header
DIP002 — Exit Node Missing

The workflow must declare an exit: field pointing to an existing node.

error[DIP002]: exit node does not exist
  --> pipeline.dip:1:1
  = help: add "exit: <NodeID>" to the workflow header
DIP003 — Unknown Node Reference in Edge

Every edge's From and To must reference existing node IDs. The validator uses Levenshtein distance to suggest corrections for typos.

error[DIP003]: unknown node reference "InterpretX" in edge
  --> pipeline.dip:45:5
  = help: did you mean "Interpret"?
DIP004 — Unreachable Node from Start

Every node must be reachable from the start node via some path of edges. BFS from the start node cannot reach this node.

error[DIP004]: node unreachable from start
  --> pipeline.dip:20:3
  = help: add an edge leading to this node, or remove it
DIP005 — Unconditional Cycle Detected

The workflow graph must be a DAG, with the exception of restart edges. A back-edge not marked restart: true would loop forever.

error[DIP005]: unconditional cycle detected
  --> pipeline.dip:50:5
  = help: remove an edge in this cycle or mark it "restart: true"
DIP006 — Exit Node Has Outgoing Edges

The exit node is the terminal — it must have zero outgoing edges.

error[DIP006]: exit node has outgoing edges
  --> pipeline.dip:55:5
  = help: remove outgoing edges from the exit node
DIP007 — Parallel/Fan-In Mismatch

Every parallel node must have a matching fan_in node with the same set of branch nodes.

error[DIP007]: parallel fan-out/fan-in mismatch
  --> pipeline.dip:15:3
  = help: add a matching fan_in node
DIP008 — Duplicate Node ID

Node IDs must be globally unique within a workflow.

error[DIP008]: duplicate node ID
  --> pipeline.dip:30:3
  = help: rename this node or remove the duplicate
DIP009 — Duplicate Edge

No two edges may have the same (from, to, condition) combination. Edges with different conditions on the same pair are not duplicates.

error[DIP009]: duplicate edge
  --> pipeline.dip:60:5
  = help: remove the duplicate edge
DIP010 — Unparseable Edge Condition

Every edge when condition must parse into a valid expression. An unparseable condition — an unknown operator, or a tool-node field like marker_grep used in operator position — leaves the edge's routing undefined, so the workflow cannot execute. One diagnostic fires per bad edge; every parseable edge is still checked.

error[DIP010]: edge A -> Z: invalid condition "marker_grep \"^ok\"": unknown operator "^ok"
  --> pipeline.dip:14:5
  = help: valid operators: = == != contains startswith endswith in

Semantic Warnings (DIP101-DIP151)

These flag likely bugs or questionable patterns. Warnings alone exit 0.

DIP101 — Node Only Reachable via Conditional Edges

All incoming edges have conditions — if none match, execution can never reach this node. Automatically suppressed when source nodes have exhaustive conditions (e.g., success/fail pairs).

warning[DIP101]: node "NextPhase" is only reachable through conditional edges
  --> pipeline.dip:25:3
  = help: add an unconditional edge, or verify conditions are exhaustive
DIP105 — No Success Path to Exit

There is no guaranteed path from start to exit through unconditional edges alone. If conditions don't match, execution may never reach the exit.

warning[DIP105]: no success path from start to exit
  --> pipeline.dip:1:1
DIP108 — Unknown Model/Provider

The model or provider isn't in the recognized list. Verified against official provider documentation.

warning[DIP108]: unknown model/provider combination
  --> pipeline.dip:15:5
DIP110 — Empty Prompt on Agent

An agent node has no prompt text. An agent without a prompt won't produce meaningful output.

warning[DIP110]: empty prompt on agent node
  --> pipeline.dip:12:3
DIP111 — Tool Without Timeout

A tool node has no timeout field. Without a timeout, a hanging command blocks the pipeline indefinitely.

warning[DIP111]: tool command has no timeout
  --> pipeline.dip:35:3
DIP115 — Goal Gate Without Recovery Path

A node has goal_gate: true but no retry_target or fallback_target, meaning the pipeline has no recovery path if the gate fails.

warning[DIP115]: node "validate_tests" has goal_gate but no recovery path
  --> pipeline.dip:18:3
  = help: add retry_target or fallback_target
DIP119 — Invalid reasoning_effort Value

The reasoning_effort field on an agent node has an unrecognized value. Valid levels: none, minimal, low, medium, high, xhigh, max. Not all providers support all levels.

warning[DIP119]: node "Analyze" has reasoning_effort "extreme" which is not a recognized level
  --> pipeline.dip:12:5
  = help: valid levels: none, minimal, low, medium, high, xhigh, max
DIP123 — Tool Command Syntax Error

The tool command block has a shell syntax error detectable by bash -n — unclosed quotes, bad redirects, missing fi/done.

warning[DIP123]: tool command has shell syntax error:
  unexpected EOF while looking for matching `"'
  --> pipeline.dip:45:5
DIP124 — Runtime Variable in Tool Command

A tool command contains ${ctx.*} interpolation. These are Dippin runtime variables that expand to empty strings in the shell.

warning[DIP124]: tool command references ${ctx.api_url}
  which expands to empty at runtime
  --> pipeline.dip:50:5
DIP125 — Binary Not Found

The first command in the tool block references a binary not on the current PATH. This is a hint — the deployment environment may differ.

hint[DIP125]: tool command binary "npx" not found on PATH
  --> pipeline.dip:55:5
DIP126 — Subgraph ref file does not exist

A subgraph node's ref: path does not resolve to an existing file on disk.

DIP127 — Invalid human node mode

The mode: value on a human node must be choice, freeform, interview, or yes_no.

warning[DIP127]: invalid human node mode "dialog"
  --> pipeline.dip:22:5
  = help: use one of: choice, freeform, interview, yes_no
DIP128 — Interview mode with meaningless default

A human node with mode: interview has a default: value, which has no effect in interview mode — answers come from the extracted question list.

warning[DIP128]: human node "GatherRequirements" has mode: interview with a default value
  --> pipeline.dip:28:5
  = help: remove the default field — it is ignored in interview mode
DIP129 — Interview mode with choice-style labeled edges

A human node with mode: interview has outgoing edges with choice labels, which conflict — interview mode collects freeform answers, not discrete choices.

warning[DIP129]: human node "GatherRequirements" uses interview mode but has choice-style edges
  --> pipeline.dip:30:5
  = help: remove edge labels or switch to mode: choice
DIP130 — Invalid response_format value

The response_format: field on an agent node must be json_object or json_schema. Any other value is unrecognised and will be rejected at runtime.

warning[DIP130]: invalid response_format "xml" on agent "Parse"
  --> pipeline.dip:18:5
  = help: use "json_object" or "json_schema"
DIP131 — response_schema / response_format mismatch

Two related hints: (1) if response_schema: is set but response_format: is not json_schema, the schema will be ignored; (2) if response_format: json_schema is set but no response_schema: is provided, the model receives no schema to enforce.

warning[DIP131]: response_schema set but response_format is not json_schema
  --> pipeline.dip:20:5
  = help: set response_format: json_schema or remove response_schema
DIP132 — response_schema is not valid JSON

The value of response_schema: must be a valid JSON object. Malformed JSON will cause a runtime error when the model attempts to apply structured output.

warning[DIP132]: response_schema on agent "Extract" is not valid JSON
  --> pipeline.dip:22:5
  = help: fix the JSON syntax in response_schema
DIP133 — params key shadows a first-class field

A key inside the agent's params: block (e.g. model or provider) duplicates a first-class field on the same node. The first-class field takes precedence; the params entry is silently ignored.

hint[DIP133]: params key "model" shadows the first-class model field on agent "Analyze"
  --> pipeline.dip:35:7
  = help: remove the params key and set the field directly

DIP134 — max_retries set with restart edges but no max_restarts

Severity: Warning

Fires when max_retries is set in defaults and the workflow has restart: true edges, but max_restarts is not set. These are commonly confused: max_retries controls per-node LLM retries, while max_restarts controls the global loop restart budget.

Fix: Set max_restarts in defaults to control loop iterations, or add it alongside max_retries if both behaviors are intended.

DIP143 — Referenced subgraph does not inherit tool_access

Severity: Hint

A manager_loop (subgraph_ref) or subgraph (ref) node references a child .dip while this workflow declares a tool_access restriction — and tool_access does not cross the file boundary. The wasm/playground lint cannot read the child, so it reminds you to give the child’s agents their own tool_access. (For the resolved cross-file check that reads the child, see DIP146.)

hint[DIP143]: manager_loop "Supervise" references subgraph "child.dip", defined in its own file; this workflow's tool_access restrictions do not extend across the subgraph boundary

The native dippin lint cross-file pass also emits DIP143 for a deep (depth ≥ 1) child that is either partial-audit (resolved — some agents restricted, at least one tool-bearing agent without a tool_access of its own) or unresolvable (missing, unparseable, or refused) — boundaries the entry-only lint never sees. Like DIP146, it fires only when a workflow on the path declares tool_access.

DIP144 — Agent node has no failure route

Severity: Warning

An agent node has no way to handle failure — no ctx.outcome = fail edge, no fallback_target, no bounded retry (retry_target + max_retries), and no graph-level on_failure. Any one of those suppresses it; an unconditional/success edge does not.

warning[DIP144]: agent node "Build" has no failure route (no fail edge, no fallback_target, no bounded retry, no graph on_failure)
  = help: add `-> <node> when ctx.outcome = fail`, set fallback_target:, add retry_target with max_retries, or declare a workflow-level on_failure:

DIP145 — Negative graph budget default

Severity: Warning

A graph budget default (max_total_tokens, max_cost_cents, max_wall_time, or stall_timeout) is set to a negative value. Budgets are non-negative; 0 (or unset) means no limit.

warning[DIP145]: workflow budget default max_cost_cents is -5; budgets cannot be negative

DIP146 — Child subgraph re-grants restricted tools (cross-file)

Severity: Hint

dippin lint resolves a manager_loop (subgraph_ref) or subgraph (ref) child across the file boundary and finds it declares no tool_access restriction on any agent, while a workflow on the path from the linted entry restricts tools. Unlike DIP143 (which cannot read the child), DIP146 reads and confirms the child and traverses transitively. The traversal is intent-aware: a child reached through both a no-intent path and a restricting path is still checked under the restricting path. Detection only — dippin flags the gap; runtime enforcement is separate.

hint[DIP146]: manager_loop "Supervise" delegates to subgraph "worker.dip", which declares no tool_access restriction on any agent; a workflow on this path restricts tools, but the restriction does not cross the subgraph boundary

DIP147 — Restricted agent’s output reaches a privileged prompt (chain attack)

Severity: Hint

A tool_access: none agent declares a context key in writes:, and a downstream tool-bearing agent (one that omits tool_access, so it holds the full catalog) declares that same key in reads:. tool_access bounds an agent’s tools, not the information its output carries — so the restricted agent’s output (potentially a laundered injection payload) reaches a privileged agent’s prompt, re-granting capability the upstream node was denied. dippin flags the explicitly-keyed handoff the author wired by hand; the runtime enforces the bound. Detection only.

hint[DIP147]: restricted agent "Summarize" (tool_access: none) writes context key "summary" that tool-bearing agent "Act" reads — its output reaches a privileged prompt

DIP148 — Negative last_response_truncate

Severity: Warning

An agent node (or a parallel-branch override) sets last_response_truncate to a negative value. The field is a character cap on the prior node’s response before it is auto-injected into this agent’s prompt (a mitigation for the ${ctx.last_response} flow); a negative cap is meaningless. On an agent, 0 (or unset) means no truncation; on a parallel-branch override, 0 (or unset) inherits the target agent’s cap. Detection only — dippin carries and lints the field; a runtime enforces the truncation.

warning[DIP148]: agent "Writer" last_response_truncate is -1; cannot be negative

DIP149 — Ambiguous routing

Severity: Warning

A node has two or more unconditional (no when) outgoing forward edges. Both are equally eligible, so which one fires is decided only by the routing cascade’s lexical tiebreak — the alphabetical order of the target node ID. That is silent action-at-a-distance: renaming a target node can change which edge fires with no other change. Restart/loop back-edges are excluded, and parallel fan-out and human choice gates are exempt. Keep one unconditional edge as the default fallback and guard the others with a when condition.

warning[DIP149]: node "Route" has multiple unconditional outgoing edges; which one fires is decided only by the lexical tiebreak

DIP150 — Human gate routes by label

Severity: Hint

An outgoing edge from a label-routing human node (mode choice, yes_no, or the unset default) sets a non-empty label: but no choice:. In Phase 0 the label is overloaded — besides being display text, it is the routing key the runtime matches the user’s selection against, so nothing in the syntax signals that the label is load-bearing. Add choice: "<key>" to mark the routing key explicitly, leaving label: for display. choice: wins when present; label: still routes when it is absent, so existing workflows route unchanged. Does not fire when choice: is already set, for non-human source nodes, or for freeform/interview gates (which route by text/answers, not labels).

hint[DIP150]: human gate "Approve" routes by label "yes"; use choice: "yes" to mark the routing key (label: stays for display)

DIP151 — Edge weight is unused by routing

Severity: Warning

An edge carries a weight: attribute. weight: was tier 4 of the routing cascade — a speculative priority hint — but the cascade never consults it and no real workflow uses it. It still parses (carry-only, no breakage in Phase 0), but both the keyword and the cascade tier are slated for removal under dip 2. Remove weight: and express priority with conditions instead — guard edges with when / on, or rely on a single unconditional fallback.

warning[DIP151]: edge "Route" -> "A" sets weight: 5, which routing does not use; guard edges with when / on or rely on a single unconditional fallback instead

Full catalog: This page highlights the most common diagnostics. For every code (DIP001–DIP010, DIP101–DIP151) with full descriptions, run dippin explain <code> or see the generated language spec. Codes DIP135–DIP142 are documented there.