Use a DSL as a Harness: Constraining What an LLM Can Generate

The problem with constraining AI through words alone

Most of the advice on this site for keeping AI-generated code within bounds is about what you say before you ask for the change: a clear Purpose and Behaviour, the decisions you settle rather than leave to the model, the Context & Constraints you state up front. That discipline works, but it's still a specification written in natural language, and natural language leaves room for a model to choose one of many valid ways to satisfy it. A general-purpose language like Python or JavaScript gives an LLM dozens of equally "correct" ways to express the same intent - different structures, different naming, different levels of abstraction - even when your prompt was precise.

There's a different kind of constraint worth knowing about: instead of only describing what you want in prose, you restrict the language the AI is allowed to generate in. This is the idea behind a recent Martin Fowler article by Unmesh Joshi, "DSLs Enable Reliable Use of LLMs," which argues that a domain-specific language (DSL) can act as a harness that guides an LLM's output far more tightly than a prompt or spec can on its own.

What a DSL does that a spec doesn't

A DSL is a small, deliberately narrow language for one domain - not a general-purpose programming language. Joshi points to familiar examples: Mermaid and PlantUML for diagrams, SQL for queries, Kubernetes YAML for infrastructure. None of these let you express arbitrary programs. Each one expresses a limited set of concepts in a fixed, constrained syntax.

Joshi's observation is that this narrowness is exactly why LLMs are so reliable at generating in these languages: a general-purpose language offers many valid ways to say the same thing, but a DSL strips that variation away. Give a model a handful of in-context examples of the DSL and it can reliably reproduce the correct syntax, because there isn't much room for it to improvise. He does note a caveat worth keeping in mind: frontline models have already seen enormous amounts of Mermaid, SQL and similar formats in training, so part of their reliability may come from familiarity rather than the DSL's structure alone. He flags that it's genuinely unclear how well this holds up for a truly novel DSL that a model has never encountered, particularly a smaller or more constrained model.

Why this matters more once you're running an agent loop

The site's articles on agentic workflows point out that an agent reporting success only tells you it satisfied its own understanding of the task - see "Agents Still Need Someone to Decide What 'Done' Means". Joshi's article adds a structural reason a DSL helps here specifically: a DSL "almost always ships with a deterministic validator" - a parser, a JSON schema, a type checker, a compiler. An agent working in a generate-and-check loop can produce a candidate in the DSL, run it against that validator, and repair it from the error message, without a human reviewing every intermediate step. That's a different kind of safety net from a human reading generated code: it's mechanical, and it catches a category of error (invalid structure, wrong shape) before a person ever looks at it.

This doesn't replace the Test & Correct work of checking that behaviour matches what you actually wanted - a DSL validator confirms the output is well-formed, not that it does the right thing. But it does mean fewer malformed candidates ever reach the point of human review.

Why building the DSL is itself part of the design work

It would be a mistake to read this as "define a DSL and the constraint problem is solved." Joshi's article makes a point that fits squarely with the argument in "Why AI Software Projects Make Fast Progress - Then Stall": a specification written before any implementation exists is, in his words, a "starting hypothesis," not a finished blueprint - many of the small decisions a system needs only surface once you're building it. He argues that reviewing generated code is not the same activity as writing it, because writing forces you to actually decide where a responsibility belongs and what boundary a piece of the system should expose. Reading code, by contrast, mostly checks whether it matches an intent you already had, without forcing those decisions.

His account of building Tickloom - a domain model and DSL for describing distributed-system behaviour - treats the LLM as useful in two different roles at two different stages: first as a brainstorming partner while the vocabulary of the domain is still being worked out, and only once that vocabulary is established, as a natural-language interface for using it. In other words, the DSL isn't something you hand to an LLM to design; you (with the model's help exploring the space) design it, and only afterwards does the model get to work reliably inside it.

Where this fits alongside PDAID

The seven PDAID phases don't currently name a DSL as a technique, and this article isn't claiming the book does - it's a distinct, complementary idea worth adding to the toolbox. But it maps naturally onto Context & Constraints. Constraints, in PDAID terms, define the boundaries an implementation must respect. A DSL is one concrete, mechanical way to express those boundaries: instead of writing "constraints" as a paragraph of prose the model might interpret loosely, you express them as a grammar it cannot violate without producing something a validator rejects. Purpose and Behaviour still come first - a DSL restricts how something can be expressed, not why it's needed or what it should do. You still need to decide those before reaching for a constrained language to build in.

It's also worth being honest about the cost. Designing a DSL, its parser or schema, and its validator is real engineering work, and it only pays off where you have a recurring, well-bounded domain of operations to express - Joshi's examples (diagrams, queries, infrastructure manifests, distributed-system test scenarios) are all cases with a narrow, stable vocabulary that gets used repeatedly. A one-off feature in an ordinary application doesn't usually justify inventing a language for it; the behaviour template and constraints you'd write by hand are the right tool there. This is a technique for the parts of a system where the same shape of decision recurs often enough that a small language pays for itself, and where unrequested changes creeping outside a boundary are expensive enough to be worth preventing structurally rather than catching after the fact.

What to take from this if you're not building a DSL today

Even without building one, the underlying point is useful on its own: the more constrained the language you ask an LLM to generate in, the fewer ways it has to quietly diverge from what you meant. That's the same reasoning behind favouring the smallest useful increment and writing Behaviour in testable, observable terms rather than vague description - both reduce the model's room to guess. A DSL is what that same instinct looks like when you push it into the structure of the code itself rather than the words of the prompt. It's a technique worth keeping in reserve for the parts of a project with a genuinely recurring domain vocabulary, not a default for every increment.

All articles