Building AI Agent Workflows With Vercel’s AI SDK: A Practical Guide

There is a new trend in almost every corner of the internet, and it revolves around AI Agents. In case you are new to this topic, an AI Agent is an implementation of LLM communication, where an LLM is provided with tools, accesses, and decision-making abilities. Think of it as a remote working freelancer.

This technology and concept are powerful by themselves, but joining those Agents in teams is where they get insanely powerful. Those teams are usually called “Workflows”. You provide a task to workflow, and your Agents work together to provide you with a satisfying result.

What are AI Agent Workflows?

I am sure you understand the concept of Agents working together. In the real world, we have well-known organizational structures in place, and we don’t even think about them. We will implement and talk about those structures (or ‘workflows’) in a moment. If you want a bit more in-depth explanation, the read this fantastic article from Anthropic.

Getting started with AI Agent Workflows is easier than you think

It’s easy to think that implementing such an agent workflow must be a very complex and technical task. Of course, some technicality is required. But if you have any experience with programming (a.k.a. run a hello world with typescript project) and you know English (assuming you read this article, you can), then, believe it or not, you are good to go.

There are a lot of resources and frameworks on the internet that can help, but in this article, I am going to present a solution created with AI SDK from Vercel.

Building AI Workflows step-by-step

We will be working on a simple node project with AI SDK as a dependency. So start up a new project, and feel free to follow along. Our setup is extremely simple.

import { openai } from '@ai-sdk/openai'
import { generateObject,} from 'ai'
import { z } from 'zod'
const model = openai('gpt-4o')
view raw chain_1.ts hosted with ❤ by GitHub

Chaining

Chaining Workflo

Think of a situation where you’d like to apply for a loan. You start at Agent A, and after that, your application is passed along to Agent B for initial screening. Agent B can either deny you a loan or pass it along for a more in-depth check, etc. It’s all processed synchronously, one step at a time.

We will start with the first point of contact: the agent receiving a conversation with a client and then passing it along for further inspection. We will implement an agent that gathers information with a different workflow later.

const firstResponse = await generateObject({
model,
system:
'You are a first point of contact for a loan company. Your job is to turn client conversation into loan application.',
schema: z.object({
name: z.string(),
loan_amount: z.number(),
loan_time_in_months: z.number(),
monthly_income: z.number(),
}),
messages: [
{
role: 'user',
content: `
Hi! My name is Kewin.
I'd like to ask for a loan.
I need 2000$.
I can pay it back in a year.
My salary is 3000$ a month
`,
},
],
})
view raw chain_2.ts hosted with ❤ by GitHub

This agent prepares a nice object for us to work with further down the chain.

The outcome of a Chain Workflow

Now that we have that, we can implement a “gate.” A gate is an agent that decides whether to continue or stop the chain based on the input values and system prompts.

const gateResponse = await generateObject({
model,
system:
'You are a loan specialist. Based on the given json file with client data, your job is to decide if a client can be further processed.',
schema: z.object({
is_client_accepted: z.boolean(),
denial_reason: z
.string()
.optional()
.describe('If client is rejected, you need to give a reason.'),
}),
messages: [{ role: 'user', content: JSON.stringify(firstResponse.object) }],
})
console.log(gateResponse.object)
view raw chain_3.ts hosted with ❤ by GitHub

And it looks like my application passed through the gate!

The result of a Chain Workflow

Believe it or not, but this is the chain workflow. We get the response from one model, and then pass it to the next one. Repeat ad infinitum, and your chain can be as complex or as simple as you need.

Routing

Routing Workflow

You must have called a call center or customer support at some point in your life. Imagine meeting Agent A, who decides who to contact you with. You can be ‘routed’ to Agent B, C, or D. After they resolve your issue, your call ends.

This time, we will start by making the Agents to whom our call will be dispatched. Here are our Tech, Account, and Finance specialists:

const agentTypes = ['technical', 'account', 'finance', 'unknown'] as const
const agents = {
technical: 'You are a tech specialist. Clients call you with technical problems.',
account: 'You are an account manager. Clients call you with topics related to their account.',
finance: 'You are finance specialist. Clients call you with finance related topics.',
}
view raw routing_1.ts hosted with ❤ by GitHub

In this example, our specialists are simple system prompts. You could, however, improve that with their own tool calls, or other improvements. As an example, this is enough.

Now that we have our specialists, let’s prepare our dispatcher.

const routingResponse = await generateObject({
model,
system:
'You are a first point of contact for a call center. Your job is to redirect the client to a correct agent.',
schema: z.object({
agent_type: z.enum(agentTypes),
}),
messages: [
{
role: 'user',
content: `
Hi! I want to cancel my subscription now! I am very unhappy and mad.
`,
},
],
})
view raw routing_2.ts hosted with ❤ by GitHub

With that simple setup, I tested a couple of angry clients to check the response from the dispatcher. Here are the results:

The result of a Routing Flow

It looks like our dispatcher understands clients’ needs. That’s awesome. But now we need to pass the call! So let’s connect them with their Agent.

if (routingResponse.object.agent_type === 'unknown') {
process.exit(1)
}
const response = await generateText({
model,
system: agents[routingResponse.object.agent_type],
messages: [
{
role: 'user',
content: `Hi! I want to cancel my subscription now! I am very unhappy and mad.`,
},
],
})
view raw routing_3.ts hosted with ❤ by GitHub

Now, our Agent is correctly passing clients to the required specialist. That’s the idea behind Routing Agents. Of course, there are different techniques and different models, but this will work as a simple implementation.

Parallelization

Parallelization Workflow

Think of a translation task. You rarely have one person who speaks three languages, not to mention more. So, you pass your article to Agents A, B, and C, who are specialists in their respective languages. After they finish work, you receive an article in multiple languages.

So first, let’s create our specialists who will work in parallel and pass each of them the same message. Remember that the keyword here is “parallel.”

const userMessage: CoreMessage = {
role: 'user',
content: `
In this article we will cover AI Agents workflows. We hope to provide an explanation, but be aware, that in-depth explanation
would be too long for all of them. So this time, we will focus on the idea behind workflows. In future, we can make separate articles
and implement a more complex case, or even a working application for each of them.
`,
}
const germanResponse = generateText({
model,
system:
'You are German Translator. Your job is to translate text received from user, and translate it to German. Respond only with translation!',
messages: [userMessage],
})
const spanishResponse = generateText({
model,
system:
'You are Spanish Translator. Your job is to translate text received from user, and translate it to Spanish. Respond only with translation!',
messages: [userMessage],
})
const polishResponse = generateText({
model,
system:
'You are Polish Translator. Your job is to translate text received from user, and translate it to Polish. Respond only with translation!',
messages: [userMessage],
})
const [german, spanish, polish] = await Promise.all([
germanResponse,
spanishResponse,
polishResponse,
])
view raw parallel_1.ts hosted with ❤ by GitHub

Here, we trigger all our translators at once. Now, we need an Agent that Anthropic article calls an Aggregator. So, let’s ask him to make small corrections and give us all the translations.

const aggregateResponse = await generateObject({
model,
system: `You are tasked with aggregating translations of an article.
You can provide additional tweaks and fixes to formatting or translation.
You will receive a message with original, and then translations done with previous agents`,
schema: z.object({
original_text: z.string(),
german_translation: z.string(),
spanish_translation: z.string(),
polish_translation: z.string(),
}),
messages: [
{
role: 'user',
content: `
original: ${userMessage.content}
---
german: ${german.text}
---
spanish: ${spanish.text}
---
polish: ${polish.text}
`,
},
],
})
view raw parallel_2.ts hosted with ❤ by GitHub

And with that code, in a much shorter amount of time, we received all the translations:

An outcome of a parallel workflow

You might wonder why I passed all of those translations to the Agent in the end, instead of just pushing them into object. Well, as much I love AI Agents, they still can hallucinate, and add some boilerplate to the translation, like “Happy to help with translation, here it is:”. Aggregating Agents at the end grants us additional protection.

Orchestrator-Workers

Orchestrator-Workers Workflow

Let’s think of a project team. Our team will consist of a QA, a Developer, and a DevOps. We pass our feature request to our virtual Tech Lead, and he splits the tasks for estimation. Later, they estimate their tasks and get back to the Scrum Master to give us the final estimation in man days. Let’s start with the Tech Lead!

const pmMessage: CoreMessage = {
role: 'user',
content: `I want to add a simple account details screen in our NextJS application.`,
}
const techLeadResponse = await generateObject({
model,
system: `
You are a tech lead. You receive a message from the PM,
Your job is to delegate tasks to your team made of a QA, Developer and DevOps.
Split tasks for them to estimate.
`,
schema: z.object({
qa_task: z.string(),
developer_task: z.string(),
dev_ops_task: z.string(),
}),
messages: [pmMessage],
})

After the Tech Lead finishes splitting into tasks, let’s use what we learned with our parallel workflow and pass the tasks to the team members.

const responseSchema = z.object({
man_days_estimate: z.number(),
reasoning: z.string(),
})
const qaResponse = generateObject({
model,
system: `
You are a QA specialist. You receive a message from your Tech Lead,
Your job is to estimate given task in man days, and provide reasoning behind it.
`,
schema: responseSchema,
messages: [
{
role: 'user',
content: `Task from your Tech Lead:
${techLeadResponse.object.qa_task}`,
},
],
})
const developerResponse = generateObject({
model,
system: `
You are a Developer. You receive a message from your Tech Lead,
Your job is to estimate given task in man days, and provide reasoning behind it.
`,
schema: responseSchema,
messages: [
{
role: 'user',
content: `Task from your Tech Lead:
${techLeadResponse.object.developer_task}`,
},
],
})
const devOpsResponse = generateObject({
model,
system: `
You are a Dev Op. You receive a message from your Tech Lead,
Your job is to estimate given task in man days, and provide reasoning behind it.
`,
schema: responseSchema,
messages: [
{
role: 'user',
content: `
Task from your Tech Lead:
${techLeadResponse.object.dev_ops_task}
`,
},
],
})
const [qa, developer, devops] = await Promise.all([qaResponse, developerResponse, devOpsResponse])

Now that we have all the responses, let’s pass them to our Scrum Master for the final report.

const scrumMasterResponse = await generateObject({
model,
system: `
You are a Scrum Master in team. You receive estimations from your team.
Your task is to summarize the responses, voice issues, give feedback, and prepare the raport for PM
`,
schema: z.object({
final_estimation: z.number(),
issues: z.string().optional(),
feedback: z.string(),
raport_for_pm: z.string(),
}),
messages: [
{
role: 'user',
content: `
Task from pm: ${pmMessage.content}
---
QA response:
${JSON.stringify(qa.object)}
---
Developer response:
${JSON.stringify(developer.object)}
---
Dev Ops response:
${JSON.stringify(devops.object)}
`,
},
],
})

And now, let’s run our workflow. Our Tech Lead prepares the tasks for the team:

Task prepared for by the AI Agent acting as a Tech Lead in an Orchestrator Workflow

After that, our team gives their estimations and reasoning behind it:

Task estimate prepared with the support of AI Agents in an Orchestrator Workflow

At the end, the Scrum Master compiles all the results into a report:

A report prepared by an AI Agent acting as a Scrum Master in an Orchestrator Workflow

Should man days be summed? I’m not sure about it. Maybe we should find a better Scrum Master or explain the details of his work a little bit better! Whether we are happy with that result or not, this is how this workflow can be implemented. It’s basically a mix of routing and parallelization.

Evaluator-Optimizer

Evaluator-Optimizer Workflow

Writing an article is never an easy task. So you give it to Agent A - and to ensure that the paper is of good quality, you also employ Agent B to evaluate and provide feedback. Those two Agents work until Agent B is finally happy with the work and gives you the results.

Let’s create our writer, Kewin, who is tasked with writing an article about AI Agents. He has an additional extreme condition to fulfil: He has to write his article in five sentences!

const messages: CoreMessage[] = [{ role: 'user', content: "Let's see your first draft!" }]
const kewinsArticle = await generateText({
model,
system: `
You are a writer. You write articles about AI.
Your task is to write an article about AI agents, and to do it in only 5 sentences!
You might get additional feedback from your supervisor!
`,
messages,
})
view raw evaluator_1.ts hosted with ❤ by GitHub

Now, let’s create his supervisor.

const supervisorFeedback = await generateObject({
model,
system: `
You are a writers supervisor! Your agency specializes in 5 sentence long articles!
Your task is to evaluate given work, and provide feedback for improvements!
Repeat for as long as the article doesn't satisfy your requirements!
`,
schema: z.object({
feedback: z.string(),
satisfied: z.boolean(),
}),
messages,
})
view raw evaluator_2.ts hosted with ❤ by GitHub

Now that we have them, we need to put them in a loop! Just for the safety of my credit card, let’s set a breaking point at 6 repetitions.

let satisfied = false
let repetitions = 0
while (!satisfied && repetitions < 6) {
const kewinsArticle = await generateText({
model,
system: `You are a writer. You write articles about AI.
Your task is to write an article about AI agents, and to do it in only 5 sentences!
You might get additional feedback from your supervisor!`,
messages,
})
messages.push({ role: 'assistant', content: kewinsArticle.text })
const supervisorFeedback = await generateObject({
model,
system: `
You are a writers supervisor! Your agency specializes in 5 sentence long articles!
Your task is to evaluate given work, and provide feedback for improvements!
Repeat for as long as the article doesn't satisfy your requirements!
`,
schema: z.object({
feedback: z.string(),
satisfied: z.boolean(),
}),
messages,
})
if (supervisorFeedback.object.satisfied) {
satisfied = true
}
messages.push({ role: 'user', content: supervisorFeedback.object.feedback })
repetitions = ++repetitions
}
view raw evaluator_3.ts hosted with ❤ by GitHub

Now, let’s see what our article looks like!

An article written and reviewed by the Evaluator-Optimizer AI Agent

It looks like our writer is much better than we expected! He convinced his supervisor in just two tries! This is how the Evaluator-Optimizer works in practice.

Summary

As you noticed, I allowed myself some freedom in choosing a case. However, all the rules and ideas are present in those implementations. You can freely expand and make them more complex or simplify them. You can also join them together and use in any configuration you need. Workflows are a powerful way of getting your AI Agents to cooperate to finish more complex tasks - and I encourage you to try them yourself, e.g., by playing with the examples from this article.

Insights

Learn more about AI

Building a Software Factory: Write Specs an Agent Can Build From — cover

Nov 6Article

Building a Software Factory: Write Specs an Agent Can Build From

Learn how to research requirements, resolve open decisions, and write testable specs that AI coding agents can implement without the planning conversation.

 Large-Scale Migrations With Coding Agents: Planning, Verification, and Rollback — cover

Nov 4Article

Large-Scale Migrations With Coding Agents: Planning, Verification, and Rollback

Plan codebase migrations with coding agents using reviewable requirements, verification checks, staged rollouts, and defined rollback criteria.

Conversational Banking: From Customer Questions to Authorized Actions — cover

Nov 2Article

Conversational Banking: From Customer Questions to Authorized Actions

Explore conversational banking architecture through a card-dispute example, covering tool access, authorization, execution, observability, and human handoff.

Building a Software Factory: The Night My Agents Waited for Me — cover

Oct 28Article

Building a Software Factory: The Night My Agents Waited for Me

Learn how permissions, review limits, and missing product decisions stalled two overnight coding agent runs, and how their maintainer revised the workflow.

Your AI Coding Agent Still Needs an Architecture — cover

Oct 22Article

Your AI Coding Agent Still Needs an Architecture

Use explicit contracts, behavior tests, and dependency checks to guide AI coding agents and review their changes through a TypeScript favorites example.

cover

Oct 15Live Stream

Who Left the Agents Running? React Native Software Factories

Join Kewin Wereszczyński and Szymon Chmal to explore what can go wrong in a React Native software factory overnight and the controls that keep agents on track.

cover

Oct 14Podcast

When Coding Gets Faster, What Breaks Next?

Matt Hargett explains how AI moves bottlenecks across testing, deployment, infrastructure, support, and the rest of software delivery.

cover

Oct 12Podcast

AI Coding Agents and Hermes With Tzvetan Mikov

Tzvetan Mikov discusses AI coding agents, context switching, low-level engine optimization, reviewing specs and tests, and his plans for Hermes X.

cover

Oct 12Podcast

State Machines for AI Agents With David Khourshid

David Khourshid explains state machines for AI agents, human approval steps, tracing, and handling invalid model outputs in agent workflows.

cover

Oct 9Article

How to Maximize AI SDLC? Measure the Outer Business Loop, Not the Inner Developer Loop

Connect AI-assisted development to measurable business outcomes. Matt Hargett shares a checklist for assessing user exposure, results, and delivery costs.

cover

Oct 7Talk

Automating Mobile QA with Cloud Agents

Michał Pierzchała shows how cloud-based QA agents can test real iOS and Android user flows and bring screenshots, recordings, and logs into pull requests.

cover

Oct 1Live Stream

Can Jev Make AI Agent Decisions Faster?

Explore Jev with Kewin Wereszczyński and Szymon Chmal: how it works, its approach to faster agent decisions, and possible local alternatives.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.