CrewAI Tutorial (TypeScript): parsing structured output for intermediate developers

By Cyprian AaronsUpdated 2026-04-21
crewaiparsing-structured-output-for-intermediate-developerstypescript

This tutorial shows you how to make a CrewAI TypeScript agent return structured JSON, validate it, and safely parse it into a typed object. You need this when your downstream code expects predictable fields instead of free-form LLM text, especially for workflows like claims intake, KYC extraction, or ticket triage.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • A CrewAI-compatible TypeScript project
  • An OpenAI API key in OPENAI_API_KEY
  • crewai installed in your project
  • zod installed for runtime validation
  • A working tsconfig.json with esModuleInterop enabled

Step-by-Step

  1. Install the dependencies and set up your environment.
    You want the agent to produce machine-readable output, so install both CrewAI and a schema validator.
npm install crewai zod
npm install -D typescript tsx @types/node
  1. Define the schema you expect back from the agent.
    This is the contract between the LLM and your application. Keep it strict so you catch bad outputs early.
import { z } from "zod";

export const ClaimSummarySchema = z.object({
  policyNumber: z.string(),
  claimantName: z.string(),
  incidentDate: z.string(),
  severity: z.enum(["low", "medium", "high"]),
  summary: z.string(),
});

export type ClaimSummary = z.infer<typeof ClaimSummarySchema>;
  1. Create an agent and task that explicitly requests structured JSON.
    The key is to tell the model exactly what shape you want and to keep the response focused on that shape.
import { Agent, Task, Crew, Process } from "crewai";

export const analyst = new Agent({
  role: "Claims analyst",
  goal: "Extract structured claim details from messy intake notes",
  backstory: "You turn unstructured insurance notes into clean JSON.",
});

export const task = new Task({
  description: `
Extract these fields from the intake note:
- policyNumber
- claimantName
- incidentDate
- severity (low|medium|high)
- summary

Return ONLY valid JSON matching this shape:
{
  "policyNumber": string,
  "claimantName": string,
  "incidentDate": string,
  "severity": "low" | "medium" | "high",
  "summary": string
}
`,
  expectedOutput: "Valid JSON object with claim summary fields",
  agent: analyst,
});
  1. Run the crew and parse the result with Zod.
    CrewAI returns text, so treat the output as untrusted until validation passes. If parsing fails, you can reject the response or retry with a stricter prompt.
import { ClaimSummarySchema } from "./schema";
import { analyst, task } from "./crew";

async function main() {
  const crew = new Crew({
    agents: [analyst],
    tasks: [task],
    process: Process.sequential,
  });

  const result = await crew.kickoff({
    inputs: {
      note: `
Policy #CLM-10091.
Claimant: Sarah Okafor.
Incident happened on 2024-11-02 after a kitchen fire.
Severity appears high because there is structural damage.
`,
    },
  });

  const raw = String(result);
  const parsed = ClaimSummarySchema.parse(JSON.parse(raw));

  console.log(parsed.policyNumber);
  console.log(parsed.severity);
}

main();
  1. Add a defensive parser for real-world outputs.
    In production, models sometimes wrap JSON in markdown fences or add extra text. Strip those wrappers before parsing so your pipeline is less brittle.
export function extractJson(text: string): string {
  const fenced = text.match(/```json\s*([\s\S]*?)\s*```/i);
  if (fenced?.[1]) return fenced[1].trim();

  const firstBrace = text.indexOf("{");
  const lastBrace = text.lastIndexOf("}");
  if (firstBrace >= 0 && lastBrace > firstBrace) {
    return text.slice(firstBrace, lastBrace + 1).trim();
  }

  throw new Error("No JSON object found in model output");
}
  1. Put it together in one executable file.
    This version handles raw model output more safely and gives you a typed object you can pass into your app.
import { Agent, Task, Crew, Process } from "crewai";
import { z } from "zod";

const ClaimSummarySchema = z.object({
  policyNumber: z.string(),
  claimantName: z.string(),
  incidentDate: z.string(),
  severity: z.enum(["low", "medium", "high"]),
  summary: z.string(),
});

function extractJson(text: string): string {
  const fenced = text.match(/```json\s*([\s\S]*?)\s*```/i);
  if (fenced?.[1]) return fenced[1].trim();
  const firstBrace = text.indexOf("{");
  const lastBrace = text.lastIndexOf("}");
  if (firstBrace >= 0 && lastBrace > firstBrace) return text.slice(firstBrace, lastBrace + 1).trim();
  throw new Error("No JSON object found in model output");
}

async function main() {
  const analyst = new Agent({
    role: "Claims analyst",
    goal: "Extract structured claim details from messy intake notes",
    backstory: "You turn unstructured insurance notes into clean JSON.",
    verbose: false,
    allowDelegation: false,
    llmConfig: {
      modelName: process.env.OPENAI_MODEL ?? "gpt-4o-mini",
      apiKey: process.env.OPENAI_API_KEY!,
    },
  });

const task = new Task({
    description:
      'Return ONLY valid JSON with keys policyNumber, claimantName, incidentDate, severity, summary.',
    expectedOutput: "JSON object",
    agent: analyst,
});

const crew = new Crew({ agents:[analyst], tasks:[task], process: Process.sequential });
const result = await crew.kickoff({ inputs:{ note:"Policy CLM-10091; Sarah Okafor; fire on Nov 2, high severity." } });

const parsed = ClaimSummarySchema.parse(JSON.parse(extractJson(String(result))));
console.log(parsed);
}

main();

Testing It

Run the script with npx tsx src/index.ts or whatever entrypoint you used. If everything is wired correctly, you should get a parsed object printed to stdout instead of raw prose.

Test three failure modes before shipping it:

  • malformed JSON
  • missing required keys
  • invalid enum values like "critical" instead of "high"

If any of those fail validation, that is good behavior. It means bad model output stops at the boundary instead of leaking into your business logic.

Next Steps

  • Add retry logic that re-prompts the agent when Zod parsing fails.
  • Move from JSON.parse to a stricter output parser if your CrewAI version supports native structured outputs.
  • Wrap this pattern in a reusable utility for every extraction workflow in your codebase.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides