Skip to content
Two Months of Dippin: Edges Own Everything Now

Two Months of Dippin: Edges Own Everything Now

← All Writing
Products mentioned in this post
Dippin — A domain-specific language for authoring AI pipeline workflows — typed syntax for prompts, models, and branching that DOT can't express.

Quick refresher if you missed the original announcement: Dippin is our domain-specific language (DSL) for writing AI pipeline workflows. It replaced Graphviz DOT for us. A multi-agent pipeline is a graph, but its nodes are programs, and DOT makes you cram prompts and shell scripts into escaped string attributes. Dippin gives those things real syntax, plus a validator with a diagnostic catalog, a formatter, a Language Server Protocol (LSP) server, and a simulator.

In the two months since, we’ve shipped eight releases, v0.43 through v0.50, most of them chasing one big project we’ve been calling the routing cleanup. The language got smaller in the ways that matter and stricter in the ways that matter more.

dip 2: all routing lives in one place

The headline is dip 2, an opt-in version header with a single organizing principle: edges own destinations. All of them.

Dippin v1 inherited a couple of node-level routing knobs (retry_target: and fallback_target:) that let a node declare where it goes on failure, as a field on the node. Convenient, but not great: it meant a graph’s structure lived in two places, and reading the edges block didn’t tell you where control could flow. Under dip 2, those fields are rejected. A failure destination is an on fail edge, a retry is a loop edge, and the edges block is the complete truth about the graph.

Nobody has to rewrite anything by hand. dippin fmt --migrate converts v1 files for you. It’s conservative: when it can translate a case one-to-one, it does. When it can’t, it leaves the original untouched, flags it with an inline # MIGRATION: comment, and exits with a distinct code1, so your continuous-integration (CI) checks know a human needs to look.

The same principle killed another duplication. A parallel Fan -> A, B node declares its fan-out inline, but workflows were routinely re-declaring those same edges in the edges block and keeping the two in sync by hand. We audited every part of the toolchain that used that information, confirmed the inline list was already the source of truth, and made it official: the formatter now strips the redundant copies, a new lint flags them, and dip 2 rejects them outright. Our own example workflows lost 216 redundant edge declarations in one sweep.

Every fallback is now one line

Real workflows have a shape: a dozen or so nodes, each with a couple of guarded edges, and every one of them needing a fallback edge you add yourself (pointing to the same cleanup node) for when none of the guards match. Those repeated fallbacks added more lines to our production pipelines than anything else.

It’s one line now:

  edges
    Analyze -> Fix when ctx.outcome == "fail"
    Analyze -> Ship when ctx.outcome == "success"
    else -> Cleanup

A section-level else -> is the graph’s default destination for any node whose guards all miss. The validator treats it as reachable, the linter stands down on nodes it covers, and the simulator routes through it exactly like the engine does. Deliberately, it’s success-side only. A node failure still routes through the failure cascade, because “no guard matched” and “the tool crashed” are different events, and blurring the two is how failures slip through unnoticed.

Prompts stopped drifting

The other big cleanup targeted duplication of a different kind. We found the same control-protocol boilerplate, the “your final line MUST be exactly STATUS: …” contract, hand-pasted 65 times across 11 files in one of our pipeline repos, each copy drifting a little.

Now the defaults block takes a prompt cascade: declare prompt_prefix / prompt_suffix once, inline or loaded from a shared fragment file, and it applies to every agent in the workflow (with the suffix guaranteed to be the final content) so last-line contracts hold. Individual agents can opt-out or append their own fragment. Fragment files get the same security envelope as everything else Dippin loads.2

Related: dippin pack --no-inline can now ship a workflow’s sibling assets (helper scripts, shared fragments) inside the .dipx bundle3 as real files, so a packed run resolves relative paths byte-for-byte identically to a source-tree run.

And the trust work

The least glamorous release might be the most important one. Double-quoted edge conditions with escaped characters inside them were being quietly corrupted during parsing: when ctx.msg = "hello \"world\"" could get mangled into something that failed validation, or worse, meant something else. v0.49 made the quoted path escape-aware end to end, and made the lexer reject an unterminated string at its opening quote instead of helpfully fabricating a closing one.

That’s the pattern across all eight releases. The diagnostic catalog grew from 61 to 64 codes, and every new one is conservative by design: documented to have no false positives, with a test suite guard proving our own examples stay clean. The migration tool exits loud when it’s unsure. The pack command refuses rather than guessing. For a language whose programs spend real money when they run, we think boring strictness is the feature.

Everything above is in dippin v0.50: brew install 2389-research/tap/dippin-lang, or go install github.com/2389-research/dippin-lang/cmd/dippin@latest. The full changelog has the details, at our customary excessive length.


  1. Exit 3 (ExitMigrateReview), distinct from the exit 1 you get for ordinary formatting drift, so a completed-migration check can tell the two apart. ↩︎

  2. The same checks every file load gets: path containment (the file has to stay inside the project directory), rejection of symlinks, and a size cap. ↩︎

  3. A .dipx is a single, content-addressed bundle of a workflow plus everything it references: the deployable artifact you ship, verified by hash, instead of a loose directory of .dip files. ↩︎

More Posts

20 pages · hugo 0.148.2 · 57b40bc · built Sep 3 18:34
2389 Radio
2389 RADIO Select a station