Dynz for Developers

Interaction logic as code.
Readable by anything.

A TypeScript-first schema language for validation, conditions, and flow. Write the contract once — as pure, serializable data — and let your frontend, your backend, and your agents evaluate the same definition.

High-code by design

This is not your headless form builder

No drag-and-drop designer. No hosted dashboard. No widget library you'll spend a sprint theming around. Dynz is high-code by conviction: you define interaction logic in TypeScript, and what you get back is a contract.

Form builders generate screens. Dynz defines the logic that screens — and servers, and agents — are built from.

Serializable

The whole contract survives a JSON round-trip.

Pure

Expressions are data, not functions — deterministic, no side effects, no hidden closures.

Headless

Zero rendering opinions. Bring your own UI, or no UI at all.

Schema definition

A schema language that feels like the tools you already use

If you've written Zod, you already know the shape. Fluent builders, composable types, full static inference — your schema is the single source of both runtime validation and TypeScript types.

The difference shows up in what happens next.

Explore the schema API
user.schema.ts
import * as d from "dynz";

const userSchema = d.object({
  name: d.string().min(2),
  email: d.string().email(),
  password: d.string().min(6),
  passwordConfirm: d.string().equals(d.ref("password")),
});

// Inferred: { name: string; email: string;
//   password: string; passwordConfirm: string }
type UserData = d.SchemaValues<typeof userSchema>;
Serializable expressions

Logic that survives the wire

In most validation libraries, the moment you need cross-field logic you reach for refine() — and hand the library a JavaScript function. Functions don't serialize. From that point on, your logic is welded to one runtime, one bundle, one side of the network.

In Dynz, conditions, validation rules, and derived values are expressions — data structures, not closures. d.ref(), d.eq(), d.and(), d.multiply() compose freely inside any rule slot. The whole schema is a plain object you can JSON.stringify, store, ship over HTTP, and evaluate on the other end.

Write it once. Any consumer that can parse JSON can enforce it.

Read about the expression system
across-the-wire.ts
// Built on the server…
const schema = d.object({
  accountType: d.options(["personal", "business"]),
  companyName: d.string().min(2)
    .setIncluded(d.eq(d.ref("accountType"), "business")),
});

// …sent as JSON…
return Response.json(schema);

// …and evaluated anywhere: browser, server, agent.
const result = d.validate(schema, undefined, values);
Conditional logic

Cross-field dependencies are first-class

Fields that appear, requirements that tighten, rules that switch based on what the user already entered — this is where real-world forms live, and where most schema libraries make you bolt logic on. In Dynz, it's the native grammar: .setIncluded(), .setRequired(), and .when() all accept the same serializable predicates.

Because conditions are data, every consumer can answer the question forms usually can't: why is this field required right now?

See conditional schemas
order.schema.ts
const orderSchema = d.object({
  orderType: d.options(["standard", "express", "international"]),
  shippingMethod: d.options([{
    value: "overnight",
    enabled: d.eq(d.ref("orderType"), "express")
  }, {
    value: "same-day",
    enabled: d.eq(d.ref("orderType"), "express")
  }, {
    value: "air",
    enabled: d.eq(d.ref("orderType"), "international")
  }, {
    value: "sea",
    enabled: d.eq(d.ref("orderType"), "international")
  }]),
  customsInfo: d.object({
    value: d.number().min(0),
    description: d.string().min(1),
  }).setIncluded(d.eq(d.ref("orderType"), "international")),
});
Mutability

Encode who can change what — and when

Most validation libraries only check whether a value is valid. Dynz also checks whether a value was allowed to change. .setMutable() takes a boolean or a predicate, so state-machine rules — drafts are editable, published records are frozen, only admins touch the title — live in the contract instead of scattered through service code.

It works down to array elements: allow appending and removing, to forbid rewriting what's already there.

Learn about mutability controls
document.schema.ts
const documentSchema = d.object({
  status: d.options(["draft", "review", "published"])
    .setMutable(false),

  content: d.string()
    .setMutable(d.eq(d.ref("status"), "draft")),
});

// validate() receives current AND new values —
// mutations are checked, not trusted
d.validate(documentSchema, currentValues, newValues);
Runtime generation

Schemas built at request time

Schemas are plain TypeScript objects — which means they don't have to be static. Generate them on the server per request: shaped by the user's role, the product's state, the tenant's configuration, this quarter's business rules. The frontend doesn't hold a copy of the logic. It receives the logic.

No redeploy to change a rule. No drift, because there's only one place the rule exists.

build-schema.ts
function buildSchema(user: { role: "admin" | "user" }) {
  return d.object({
    title: d.string().min(1)
      .setMutable(user.role === "admin"),
    content: d.string()
      .setMutable(d.eq(d.ref("status"), "draft")),
  });
}
One engine

The friendly nudge and the hard stop are the same rule

The inline error in the browser and the 422 at the API boundary come from one definition, evaluated by one engine. Client-side validation stops being a hopeful duplicate of the backend — it is the backend's rule, running closer to the user.

Nothing to keep in sync. Nothing to drift.

validate.ts
// Browser and server run the identical call
const result = d.validate(schema, currentValues, newValues);

if (!result.success) {
  // Structured, typed errors — render them,
  // log them, or hand them to an agent
  console.log(result.errors);
}
Agents

The contract your agent has been asking for

An LLM can't read your interface, and it shouldn't have to guess your rules. Because a Dynz schema is explicit, serializable data, an agent can fetch it, understand exactly which fields exist, which are required under the current answers, and what valid input looks like — then be told precisely what's wrong when it misses. The same schema that drives your web form drives your MCP integration, without modification.

Humans get a UI. Agents get the contract. Both get the same truth.

Dynz + MCP →

Get started

Install

Add Dynz to any TypeScript project. No CLI, no account, no config.

pnpm install dynz
Quick start

Define a schema, ship it over the wire, validate on both ends — in about twenty lines.

Follow the quick start
Examples

Complete working integrations, including a Next.js app rendering forms straight from a schema.

Browse examples
Source

Built in the open. Read the code, open an issue, send a PR.

github.com/rubenvanrooij/dynz

Ready to write the contract?

One schema. Every consumer. Start with a form — end up with an interface any channel can read.

Dynz.

Headless interaction logic. Written once, structured, readable by anything.