61 diagnostic codes — 10 structural errors and 51 semantic warnings — to catch problems before runtime.
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.
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"?These must be fixed for a workflow to be valid. Each causes exit code 1.
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
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
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"?
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
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"
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
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
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
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
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
These flag likely bugs or questionable patterns. Warnings alone exit 0.
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
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
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
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
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
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
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
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
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:5The 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
A subgraph node's ref: path does not resolve to an existing file on disk.
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
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
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
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"
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
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
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
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.
tool_accessSeverity: 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 boundaryThe 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.
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: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 negativeSeverity: 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 boundarySeverity: 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 promptlast_response_truncateSeverity: 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 negativeSeverity: 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 tiebreakSeverity: 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)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 insteadFull 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.