The agent returned a well-formed JSON object. The code accepted it and wrote a change. Only later did someone notice that the identifier pointed to another customer, the source version was stale, or the operation was outside that agent's authority.
Valid JSON is not trusted data. To validate AI agent output before saving it, treat the result as an untrusted proposal and pass it through four gates: shape, meaning, authority, and verified effect. A schema handles the first. The others depend on your code and on the systems that own the real state.
This article is a spoke of the guide on testing an AI agent's trajectory. A trajectory check verifies the execution path. Here, the narrower question is: can the final output cross the boundary that writes data or triggers an action?

Short answer
- Validate the received shape, even when the provider offers structured outputs.
- Check identity, evidence, version, and business rules against current state.
- Apply authorization and approval in the runtime, not in the text generated by the model.
- Stage a proposal or use an idempotent operation, then read the destination back before declaring success.
This continues the validation of tool arguments and tool responses in TypeScript, but the decision point has moved: the output has crossed execution and is about to become persistent state.
What does structured output actually guarantee?
Structured output guarantees a more predictable shape when the provider and schema are compatible. It does not guarantee that the values describe the world correctly, that the agent chose the right record, or that an external write happened.
The OpenAI Node SDK documentation on Structured Outputs, checked on 2026-10-02, shows how to obtain parsed output with Zod. It also explains that an incomplete response may have no parsed output. That changes the contract: a missing value, refusal, timeout, and invalid object are different states from an accepted result.
The Microsoft Agent Framework guide to producing structured outputs, checked on 2026-10-02, presents typed models and JSON maps as response formats. The feature helps move predictable fields between components. It does not replace checks that only the application can perform, such as verifying that a customer belongs to the current account or that a change is still authorized.
The correct boundary is therefore not “the model returned JSON.” It is:
agent output
-> shape
-> meaning
-> authority
-> safe proposal or write
-> confirmation read-back
The JSON Schema guide to required properties and additional properties, checked on 2026-10-02, shows why required and additionalProperties represent different decisions. Even an object that passes both can contain a stale value or a reference outside the current user's scope.
How do you separate shape from meaning?
Shape asks whether the object has acceptable fields, types, and structural constraints. Meaning asks whether those values fit the state controlled by the application. Mixing them often produces an enormous schema, a confusing prompt, or false confidence.
Consider an extraction result that proposes updating an order:
import { z } from "zod";
const UpdateProposal = z.object({
proposalId: z.string().min(1),
accountId: z.string().min(1),
orderId: z.string().min(1),
newStatus: z.enum(["approved", "rejected"]),
sourceVersion: z.string().min(1),
evidenceRefs: z.array(z.string().min(1)).min(1),
reason: z.string().min(1),
}).strict();
type UpdateProposal = z.infer<typeof UpdateProposal>;
The schema checks that orderId exists, that newStatus belongs to the allowed set, and that at least one evidence reference is present. It does not check that the order belongs to the account, that the source version is current, or that the agent may change that status.
The Zod guide to handling errors, checked on 2026-10-02, explains that safeParse returns a discriminated union. That shape is useful at runtime because the flow can separate result.success from result.error without turning every validation failure into a generic exception.
A meaning check can be small and deterministic:
type MeaningCheck =
| { ok: true; order: { id: string; accountId: string; status: string } }
| { ok: false; code: "unknown-order" | "wrong-account" | "stale-source" };
async function checkMeaning(
proposal: UpdateProposal,
current: {
loadOrder(id: string): Promise<{ id: string; accountId: string; status: string } | null>;
currentSourceVersion(): Promise<string>;
},
): Promise<MeaningCheck> {
const order = await current.loadOrder(proposal.orderId);
if (!order) return { ok: false, code: "unknown-order" };
if (order.accountId !== proposal.accountId) {
return { ok: false, code: "wrong-account" };
}
if (await current.currentSourceVersion() !== proposal.sourceVersion) {
return { ok: false, code: "stale-source" };
}
return { ok: true, order };
}
This code is illustrative. It shows the separation between parsing and reading current state, but it does not know your database, concurrency policy, or approval requirements. Test the function at the real boundary when a decision can produce an external effect.
Where should authorization live?
Authorization belongs after shape and meaning validation, but before the write. The agent may propose approved; it must not turn that field into permission simply by repeating the word in JSON.
The OpenAI Agents SDK guardrails guide, checked on 2026-10-02, separates input, output, and tool guardrails. That distinction matters in workflows with handoffs: an output guardrail is not automatically a check around every tool used by intermediate agents.
A decision about authority may depend on identity, scope, risk, and human approval:
type AuthorityDecision =
| { kind: "allow"; approvalId?: string }
| { kind: "deny"; reason: string }
| { kind: "needs-review"; reason: string };
function authorize(
proposal: UpdateProposal,
actor: { agentId: string; accountId: string; canChangeOrders: boolean },
): AuthorityDecision {
if (actor.accountId !== proposal.accountId) {
return { kind: "deny", reason: "account scope does not match" };
}
if (!actor.canChangeOrders) {
return { kind: "needs-review", reason: "agent lacks order-write authority" };
}
return { kind: "allow" };
}
Do not treat this example as a ready-made policy. The application must load identity from a trusted source, apply the correct scope, and record the decision. The model can suggest a reason. The reason does not replace the rule that allows or blocks the operation.
If the result is passed to another agent, the receiver must validate the package too. The article on designing an AI agent handoff with structured context covers that change in responsibility. The point here is that authorization remains a runtime decision, even when the value came from an earlier step.
When should output become a proposal?
Use a proposal when the result still needs review, when the operation is difficult to undo, or when the agent should not write directly to the destination. The proposal should keep the validated value, the references that support it, the rule version, and a stable identifier for the attempt.
The flow can be modeled like this:
agent_output
-> rejected: invalid_shape
-> rejected: invalid_meaning
-> needs_review: authority_or_risk
-> staged: proposal_created
-> applied: operation_accepted
-> verified: destination_read_back
The staged state prevents the interface from confusing an intention with a completed change. It also makes it easier to show a person exactly what will change. An approval should point to the proposalId and a hash of the relevant content, not just the tool name.
The OpenAI guide to running agents, checked on 2026-10-02, documents invalid final output errors and validated fallbacks that do not repeat tool calls. The pattern is useful: when the failure is in the final output, do not automatically turn recovery into a new run with possible duplicate effects.
I do not have a production database or real agent behind this fixture. The staging step is a design recommendation based on the verifiable boundaries above. In a real system, the team must decide which proposal types require approval and which external state can be read back.
How do you avoid a duplicate write?
A sound validation can approve an operation that fails in transport, finishes after a timeout, or is repeated by a worker. Retry control must exist in the write operation, not only in the prompt.
Use an idempotency identifier derived from the stable operation intent, destination, and proposal version. The same logical attempt should find the same operation record. A proposal whose content changed should get a new identifier, even if it targets the same order.
The article on testing API idempotency without duplicate side effects covers the boundary of a repeatable operation. For agents, the rule is the same: a model retry does not prove that the first write did not happen. Before repeating it, consult the operation record or the external destination.
The states should distinguish at least:
| State | Meaning | Safe next step |
|---|---|---|
rejected |
The object or decision failed a rule | fix the input or request review |
staged |
The proposal exists but was not applied | approve or discard |
applied |
The executor accepted the operation | verify the destination |
unknown |
The process cannot tell whether the effect happened | inspect the destination before retrying |
verified |
A destination read confirms the expected result | release the next step |
unknown is not the same as failed. A timeout after sending can hide an effect that was already applied. The Microsoft Agent Framework pipeline architecture, checked on 2026-10-02, describes middleware and persistence gates that prevent output from being released or stored before the applicable verdict. The implementation varies, but the boundary is useful.
Which cases belong in the test?
Test values that look acceptable, not only broken JSON. The dangerous failure is the one that passes the parser and reaches the write with the wrong reference, a stale version, or authority the agent does not have.
A minimum matrix includes:
- valid object, correct account, current evidence, and permitted operation;
- missing required field, invalid enum, and unexpected property;
- existing
orderIdthat belongs to another account; - stale evidence version or missing evidence reference;
- model refusal, incomplete response, and timeout before the object;
- transport failure before the write and timeout after the write;
- retry of the same
proposalIdand retry with changed content; - destination reports success but does not confirm the change on read-back.
The test should assert more than “the function returned an object.” Check that no write started when shape validation failed, that state did not change when authority was denied, and that a retry inspected the destination for an unknown result.
This complements validating external JSON before using it in TypeScript. That post explains how to treat external data as unknown. Agent output has the same problem, with added evidence, authority, and side-effect risk.
What can validation not prove?
Schema validation does not prove factual truth. It can confirm that customerId is a string, but not that the customer exists. A meaning check can confirm that the record exists, but not that the business rule was interpreted correctly. Authorization can allow an operation, but it cannot guarantee that the database applied the change.
Do not use a confidence score produced by the agent as independent proof either. It can help route cases to review, but it does not replace a source query or deterministic rule. When the application depends on evidence, keep the reference and check the content in the system that owns the data.
Do not confuse the agent result with the record of what happened. The AI agent audit-trail guide should be written by the runtime or executor that observes the decision and effect. Validated output is an input to that decision, not final execution evidence.
Frequently Asked Questions
Does structured output remove the need for validation?
No. Structured output reduces shape failures and gives the program fields it can read. The application still needs to check identity, current state, evidence, authority, and effect. An object that passes the schema can still contain the wrong customer or stale information.
Should I save the agent's raw output?
That depends on your data policy and retention purpose. For safe operation, keep the validated result, evidence references, versions, decisions, and correlation IDs. Keep raw output only when there is a defined purpose, access control, and a retention rule that fits the data.
Can I retry when the schema fails?
Sometimes. A bounded retry can correct a shape failure before any external effect. Do not repeat an operation only because confirmation took too long. Classify the failure, preserve the original proposal, and inspect the destination when the first attempt might already have been applied.
Can validation live inside the prompt?
Not as the only control. The prompt can explain the contract to the agent, but the runtime must validate the received value, apply policy, and block the write when the rule fails. The model should not be the authority that decides whether its own output is trustworthy.
Conclusion
Before saving agent output, ask four questions: does the object have the expected shape, do the values match current state, is the operation authorized, and did the destination confirm the effect? A schema solves only the first part.
Start with a discriminated result for rejected, staged, applied, unknown, and verified. Keep the proposal separate from the write. Use idempotent IDs for retries. Read the external system back before declaring success. This structure makes the agent easier to test and reduces the distance between a convincing response and a confirmed change.
How this analysis was done
Samuel Fajreldines is the accountable author of this article. The research compared current OpenAI Agents SDK, OpenAI Node SDK, Microsoft Agent Framework, Zod, and JSON Schema documentation, along with recent public discussions about agent verification. The TypeScript contract is illustrative and was not run against a provider, database, or production agent. AI assistance supported research organization, drafting, image generation, and localization; it did not provide production experience or replace source verification. The author also maintains RemoteCode as a work tool.
Sources consulted
- OpenAI, “Structured Outputs,” checked 2026-10-02.
- OpenAI Agents SDK, “Guardrails,” checked 2026-10-02.
- OpenAI Agents SDK, “Running agents,” checked 2026-10-02.
- Microsoft Agent Framework, “Producing Structured Outputs with Agents,” checked 2026-10-02.
- Microsoft Agent Framework, “Agent pipeline architecture,” checked 2026-10-02.
- Zod, “Handling errors,” checked 2026-10-02.
- JSON Schema, “Object,” checked 2026-10-02.