Skip to content
unlob

Mastra · TypeScript

Build a research agent with Mastra

Build a research agent on Mastra, combining typed tools, durable workflows and built-in memory in one TypeScript codebase.

What you are building

Your service

Agent

Instructions, model, tools

Memory

Per-thread, optional

Workflow

Durable, suspendable

Tools

webSearch

Typed in and out

assembleContext

Typed in and out

Services

Any AI SDK provider

claude-opus-5 here

unlob

Your own index

Your side of the API. unlob is one HTTPS service among the others — nothing here is specific to how our index works, so this agent survives a change of framework.

Install

Dependenciesbash
npm i @mastra/core @ai-sdk/anthropic zod

Step by step

  1. Create the tools

    createTool takes Zod schemas for both input and output. Typing the output is worth the extra minute — it is what makes a workflow that consumes the tool type-safe end to end.

    tools.tstypescript
    import { createTool } from "@mastra/core/tools";
    import { z } from "zod";
    
    export const webSearch = createTool({
      id: "web-search",
      description:
        "Search the web. Returns metadata only — never page bodies. Use " +
        "minSources: 3 for factual claims that need corroboration.",
      inputSchema: z.object({
        query: z.string(),
        minSources: z.number().default(2),
      }),
      outputSchema: z.object({
        results: z.array(
          z.object({
            url: z.string(),
            title: z.string(),
            snippet: z.string(),
            independent_sources: z.number(),
          }),
        ),
      }),
      execute: async ({ context }) => {
        const params = new URLSearchParams({
          q: context.query,
          min_independent_sources: String(context.minSources),
          limit: "8",
        });
        const res = await fetch(`https://api.unlob.com/search?${params}`, {
          headers: { "x-api-key": process.env.UNLOB_API_KEY! },
        });
        return { results: (await res.json()).results };
      },
    });
  2. Define the agent

    Mastra agents take a model from any AI SDK provider, so you are not locked to one vendor. Memory is a constructor option rather than something you bolt on.

    agent.tstypescript
    import { Agent } from "@mastra/core/agent";
    import { anthropic } from "@ai-sdk/anthropic";
    import { webSearch, assembleContext } from "./tools";
    
    export const researcher = new Agent({
      name: "researcher",
      instructions:
        "Search before you answer. Prefer claims carried by several independent " +
        "sources, and say so when a claim rests on one. Always cite URLs.",
      model: anthropic("claude-opus-5"),
      tools: { webSearch, assembleContext },
    });
  3. Give it memory, then make it durable

    Memory is a constructor option rather than something you bolt on, and a workflow wraps the agent so a long research run can suspend and resume. This is the pair of features that separates Mastra from a plain model client.

    durable.tstypescript
    import { Memory } from "@mastra/memory";
    import { createWorkflow, createStep } from "@mastra/core/workflows";
    import { z } from "zod";
    
    // Per-thread memory. Without this the agent forgets between calls, which
    // is usually mistaken for a bug.
    export const researcher = new Agent({
      name: "researcher",
      instructions: "Search before you answer. Always cite URLs.",
      model: anthropic("claude-opus-5"),
      tools: { webSearch, assembleContext },
      memory: new Memory(),
    });
    
    const research = createStep({
      id: "research",
      inputSchema: z.object({ question: z.string() }),
      outputSchema: z.object({ answer: z.string() }),
      execute: async ({ inputData }) => {
        const result = await researcher.generate(inputData.question, {
          memory: { thread: "research-1", resource: "user-1" },
        });
        return { answer: result.text };
      },
    });
    
    export const researchWorkflow = createWorkflow({
      id: "research-workflow",
      inputSchema: z.object({ question: z.string() }),
      outputSchema: z.object({ answer: z.string() }),
    })
      .then(research)
      .commit();

What happens when it runs

YouMastra agentClaudeunlobgenerate(question)instructions + toolstool callGET /searchtyped resultstool resultanswerresult.text
One turn of the loop. Search returns metadata only, so the agent spends a few hundred tokens deciding rather than tens of thousands reading.

The whole thing

Complete and runnable. Set the two environment variables and it works.

research-agent.ts — completetypescript
/**
 * A research agent on Mastra, backed by the unlob web index.
 *
 *   npm i @mastra/core @ai-sdk/anthropic zod
 *   export UNLOB_API_KEY=ulb_...
 *   export ANTHROPIC_API_KEY=sk-ant-...
 */
import { Agent } from "@mastra/core/agent";
import { createTool } from "@mastra/core/tools";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";

const headers = { "x-api-key": process.env.UNLOB_API_KEY! };

const webSearch = createTool({
  id: "web-search",
  description:
    "Search the web. Returns metadata only — never page bodies. Use " +
    "minSources: 3 for factual claims that need corroboration.",
  inputSchema: z.object({
    query: z.string(),
    minSources: z.number().default(2),
  }),
  outputSchema: z.object({
    results: z.array(
      z.object({
        url: z.string(),
        title: z.string(),
        snippet: z.string(),
        independent_sources: z.number(),
      }),
    ),
  }),
  execute: async ({ context }) => {
    const params = new URLSearchParams({
      q: context.query,
      min_independent_sources: String(context.minSources),
      limit: "8",
    });
    const res = await fetch(`https://api.unlob.com/search?${params}`, { headers });
    return { results: (await res.json()).results };
  },
});

const assembleContext = createTool({
  id: "assemble-context",
  description:
    "Build a corroborated, deduplicated context pack packed to a token " +
    "budget. Prefer this over several searches when you need source material.",
  inputSchema: z.object({
    query: z.string(),
    budget: z.number().default(3000),
  }),
  outputSchema: z.object({
    used_tokens: z.number(),
    passages: z.array(
      z.object({ url: z.string(), text: z.string(), reason: z.string() }),
    ),
  }),
  execute: async ({ context }) => {
    const params = new URLSearchParams({
      query: context.query,
      budget: String(context.budget),
      min_independent_sources: "2",
    });
    const res = await fetch(
      `https://api.unlob.com/assemble_context?${params}`,
      { headers },
    );
    return res.json();
  },
});

export const researcher = new Agent({
  name: "researcher",
  instructions:
    "Search before you answer. Prefer claims carried by several independent " +
    "sources, and say so when a claim rests on one. Always cite URLs.",
  model: anthropic("claude-opus-5"),
  tools: { webSearch, assembleContext },
});

const result = await researcher.generate(
  "What changed in the EU AI Act GPAI rules in 2026?",
);
console.log(result.text);

What will go wrong

The failures that cost an afternoon rather than a minute, because they produce something that looks like it is working.

Where this agent can go next

Every result the agent receives is a node in the coverage graph. These are the tools you can add to it without leaving Mastra — each is one more entry in the same tools list.

relatedNear-duplicatescorroborateIndependent sourcesauthoritiesAuthoritative hostsdossierThe entity briefpathA second documentassemble_contextA packed context setA search resultwhat your agent has
The two marked in blue are the tools this tutorial wires up. The other four are the same shape.

Frequently asked questions

How is this different from the Vercel AI SDK?

Mastra uses AI SDK providers underneath, so the model layer is shared. What Mastra adds is orchestration: durable workflows that suspend and resume, built-in memory, agent networks and evals. The AI SDK is a library for talking to models; Mastra is a framework for running agents.

Can I run it serverless?

Yes, and it deploys to Vercel, Cloudflare and Node servers. Durable workflows need a storage adapter to survive across invocations — in-memory works locally and will lose state on a cold start.

You need a key to run this

10,000 requests a month on the free tier, no card. Enough to build the agent and put a real evaluation set through it.