Skip to main content
@withboundary/contract exposes two entrypoints that do the same job:
// Reusable — define once, call many times.
const contract = defineContract({ name, schema, rules, ... });
await contract.accept(run);
await contract.accept(run);  // reuses definition

// One-off — define and run in one call.
await enforce(schema, run, { name, rules, ... });
Both run the same engine. Pick based on how you use the contract in your codebase.

defineContract — the default

Use when:
  • The contract is used in more than one place (multiple endpoints, multiple environments, shared across a service).
  • You want a named, typed object to pass around.
  • You want to override options per call without redefining.
// shared/contracts.ts
export const leadContract = defineContract({
  name: "lead-scoring",
  schema: leadSchema,
  rules: leadRules,
  retry: { maxAttempts: 3 },
  logger: appLogger,
});

// routes/score.ts
import { leadContract } from "../shared/contracts";
await leadContract.accept(run);

// routes/score-high-stakes.ts
await leadContract.accept(run, { retry: { maxAttempts: 5 } });  // per-call override
Every existing guide on this site uses defineContract.

enforce — the shortcut

Use when:
  • The contract is one-off (experimental notebook, a one-shot script, a migration job).
  • You don’t need to reuse the definition.
  • You prefer a flat call with all options in one place.
import { enforce } from "@withboundary/contract";

const result = await enforce(schema, run, {
  name: "ad-hoc-extraction",
  rules: [...],
  retry: { maxAttempts: 2 },
});
enforce is implemented on top of defineContract — there’s no behavioral difference. It’s just a convenience when the extra line would be noise.

Side-by-side

Same behavior, different ergonomics:
// enforce — one call
const result = await enforce(schema, run, { name: "score", rules });

// defineContract — two calls
const contract = defineContract({ name: "score", schema, rules });
const result = await contract.accept(run);
Both accept identical options (ContractOptions<T> & { name: string }). enforce’s signature re-arranges the required fields (schema + run + options), while defineContract bundles everything into one config object and returns a factory.

When to switch from enforce to defineContract

  • You start calling enforce(schema, ..., { name: "x", rules }) in two or more places. DRY it up — create a shared contract and reuse.
  • You want to wire a single logger (especially createBoundaryLogger) across many calls without repeating the option.
  • Your test suite starts needing to share the contract config with production code.

TypeScript inference

Both preserve schema-driven T inference:
const schema = z.object({ tier: z.enum(["hot", "warm", "cold"]) });

const r1 = await enforce(schema, run, { name: "x" });
if (r1.ok) r1.data.tier;  // typed as "hot" | "warm" | "cold"

const c = defineContract({ name: "x", schema });
const r2 = await c.accept(run);
if (r2.ok) r2.data.tier;  // same

What about the dashboard?

The SDK reads contractName off each event. Whether you used enforce or defineContract, the dashboard groups events by name. Keep names stable — renaming them splits your dashboard history.

See also

defineContract

Full option reference

enforce

Signature + example