The contract an agentic job runs on

Jesse James Richard
|
|
7 min read
#Method
#AI & Agents
#Architecture

The pipeline that drafts a page is a chain of generative steps. At the end of it a customer has an editable website built from a description of their business. No single prompt produces that. One agent works out the strategy, another the structure, another the copy, another the design. Each is a model doing creative work with its own strict inputs and outputs. Something has to hold them together.

The old answer

In an older system the answer would be a sequence of functions, each calling the next and passing along whatever it produced, or a chain of internal API calls doing the same across services. Either way, every step is wired directly to the step before it. Every step has to know the exact shape of what it is being handed, because there is nothing else guaranteeing it.

And in that older system there is usually a second layer between the steps, deterministic code that reshapes and repairs the objects as they pass, patching a missing field, coercing a value, fixing what the last step got wrong. That layer is exactly the one this platform spent a great deal of effort removing. The creative work is generative end to end. No deterministic function reaches in to correct what an agent produced. So if nothing repairs the objects between the steps, what keeps them valid?

The contract

The answer is a contract. It is a running object, the deterministic container the generative pipeline executes inside.

A contract is a state machine. It declares the steps the job moves through, carries the state that accumulates across them, and holds a status for where the job is. Each step declares what it needs and what it produces.

One step in the contract

export type DraftBuilderAddStepRole = "planner" | "executor" | "supervisor";export type DraftBuilderAddStepKind =	| "llm" | "tool" | "call" | "branch"	| "hook" | "validation" | "return";
export class DraftBuilderAddStep {	readonly name: string;	readonly owner: string;	readonly role: DraftBuilderAddStepRole;	readonly kind: DraftBuilderAddStepKind;	readonly purpose: string;	readonly inputPaths: readonly string[];	readonly outputPaths: readonly string[];	readonly nextSteps: readonly string[];	readonly allowedTools: readonly string[];	readonly allowedDataScopes: readonly string[];	readonly timeoutSeconds: number | null;}

A step names its inputs and outputs as paths into the shared state, names the steps it may move to next, and declares the tools and data scopes it is allowed to touch. Nothing about the meaning of the work is in there. The declaration covers position, permission and shape.

The state object is frozen on construction, and every update returns a new instance rather than mutating the one it was handed.

Why it cannot reshape what it holds

function deepFreeze<T>(value: T): T {	if (!value || typeof value !== "object") return value;	for (const child of Object.values(value as JsonObject)) {		deepFreeze(child);	}	return Object.freeze(value);}

The contract never reshapes the content, and the freeze is why it cannot. The deterministic code I removed reached into the content and changed it. This code holds the objects and leaves what they mean alone. It checks the schema at entry, and the agents keep their strict inputs and outputs with their creativity running inside those boundaries.

Why the steps stop needing each other

Because the shape is enforced at entry and guaranteed from there on, the steps stop needing to know each other. A step receives a state the contract has already validated, does its work, and hands back a state the contract validates again. It does not know or care which path produced its input, only that the input is valid.

The serial chain couples every step to its neighbor. The contract decouples them, so no step has to know the entire pipeline.

The pipeline behind the page builder declares thirty-eight of these steps, from the request arriving to the response going back.

The thirty-eight durable stages

// R1 requestReceived -> P1 preContentTypeHooks ->// I1 inventoryExtracted -> I2 contextAugmented ->// I3 pricingIntentClassified -> I4 inventoryReconciled ->// I5 planningContextPrepared -> I6 subtypeClassified ->// C1 strategyArcWritten -> C2 strategyBeatsWritten ->// C3 strategyAudited -> C4 strategyAccepted ->// C5 sectionIdeasDrafted -> C6 sectionIdeasAssembled -> C7 sectionIdeasDraftAudited ->// C8 sectionIdeasSurgicalChecked -> C9 sectionIdeasRecomposeDecided ->// C10 sectionIdeasRecomposed -> C11 sectionIdeasRecomposeAudited ->// C12 sectionIdeasRecomposeSurgicalChecked -> C13 sectionIdeasAccepted ->// C16 gapsAssessed -> gapAlignedSectionIdeas (contract-only validation node) ->// C17 copyWritten -> C18 copyDepthAudited -> C19 copyArchitectureWritten ->// C20 scaffoldCreated -> C21 scaffoldSurgicalChecked ->// C22 auditPassed -> C23 iconographyPlanned ->// A1 assemblyPrerequisites -> A2 assemblyFill -> A3 assemblyMedia ->// A4 assemblyStyle -> P2 postContentTypeHooks ->// Q1 visionQa -> Q2 contentQa -> M1 returnResponse.

No step in that list holds a reference to another. Each declares the names it may move to next, and the contract does the moving. Two of them are allowed seven minutes.

When the shape comes back wrong

A violation is loud. The schemas are explicit, so an object of the wrong shape does not get quietly patched into something usable. It throws, and the throw names what was expected against what arrived.

Loud is rarer than it sounds. Every model request is seeded with the expected response, so the model is conditioned toward the shape before it answers. A step that goes far enough off the page to break its own schema is the edge case.

And the throw is not where the job ends. A failed step retries with exponential backoff. Where retrying the whole step is the wrong answer, a narrower repair targets the part that came back wrong and leaves the rest of the object alone. Either way the run continues from where it stopped. Loud failure is what makes both of those possible. A contract that quietly patched a bad object would hand the next step something plausible and wrong, and nothing downstream would ever know.

The base durability is built on

This is the step that had to come before durability. You cannot safely move a workflow whose shape you cannot trust. Once the job runs inside a contract that is opinionated about its shape, it can leave the process it started in. It can be written down, handed off, and picked up somewhere else. The worker that picks it up knows the state is valid, because the contract is what guarantees it. Any worker, on any instance, can resume the job and stay correct.

The contract enforces the shape. A durable store, added on top of it, enforces the survival. The contract without durability is correct and cannot outlive its process. Durability without the contract keeps state with no guarantee that what it kept still makes sense.

The container is deterministic. The contents are generative.

Why I switched from a polylingual stack

#War story
#Architecture
#Testing

For the first few months the backend was written in two languages, Python for the file processor, AI service, error tracker, and MCP server, and TypeS...

Jesse James Richard

|

Jun 1, 2026
Read previous

Hiring an early engineer, or building something like this

Remote, Pacific time, full-time or contract. Get in touch.

Contact Jesse
Home
About
Contact
Sitemap
Privacy Policy
Terms of Service
Cookie Policy
The contract an agentic job runs on | Jesse James Richard