{"id":21676,"date":"2026-08-14T17:37:22","date_gmt":"2026-08-14T15:37:22","guid":{"rendered":"https:\/\/www.juust.org\/?p=21676"},"modified":"2026-08-15T22:34:56","modified_gmt":"2026-08-15T20:34:56","slug":"seo-blogger-agent-in-in-typescript-the-handoff","status":"publish","type":"post","link":"https:\/\/www.juust.org\/index.php\/seo-blogger-agent-in-in-typescript-the-handoff\/2026\/08\/","title":{"rendered":"SEO Blogger Agent in TypeScript ~ a Handoff"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\"><em>Three agents. One pipeline. Zero frameworks.<\/em><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You have seen the single-agent loop in our previous articles: a <code>while<\/code> loop that sends history to the model, executes tool calls, and keeps going until it gets a final answer. That loop is the foundation of every agent ever built.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But real-world workflows rarely use a single agent. They chain agents together: one researches, another writes, a third publishes. Each agent has a focused job, a narrow context window, and a clear output format. The output of one agent becomes the input of the next. That is a <strong>handoff<\/strong>, and it is the most practical multi-agent pattern you will ever use.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide builds a three-agent SEO content pipeline in <strong>vanilla TypeScript<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>SEO agent<\/strong> \u2014 queries live Google results via DataForSEO, scrapes top-ranking pages via Jina Reader, and produces a structured article brief.<\/li>\n\n\n\n<li><strong>Writer agent<\/strong> \u2014 takes the brief plus the scraped research and writes a full article draft.<\/li>\n\n\n\n<li><strong>Publisher <a href=\"https:\/\/www.juust.org\/index.php\/tag\/agent\/\" target=\"_blank\" rel=\"noreferrer noopener\">agent<\/a><\/strong> \u2014 formats the article into a WordPress-ready REST API payload and posts it as a draft.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Everything is on GitHub: <a href=\"https:\/\/github.com\/juustesout\/typescript-agent-with-handoff-seo-blogger\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/juustesout\/typescript-agent-with-handoff-seo-blogger<\/a> (the handoff example lives in the <code>TShandoff<\/code> folder).<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">1. Installatie \u2014 More Keys, Same Setup<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You need <strong>Node.js 18+<\/strong> and a handful of API keys. Two dev dependencies (TypeScript + <code>@types\/node<\/code>):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>git clone https:\/\/github.com\/juustesout\/typescript-agent-with-handoff-seo-blogger\ncd javascript-agents-example\/TShandoff\nnpm install\ncp .env.example .env\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Open <code>.env<\/code> and add your keys:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>OPENAI_API_KEY=sk-proj-your-key-here\nOPENAI_MODEL=gpt-4o-mini\nOPENAI_BASE_URL=https:\/\/api.openai.com\/v1\n\nDATAFORSEO_API_KEY=your-dataforseo-key\nDATAFORSEO_BASE_URL=https:\/\/api.dataforseo.com\n\nJINA_API_KEY=your-jina-key\n\nWP_BASE_URL=https:\/\/your-site.com\nWP_USERNAME=your-wordpress-username\nWP_APPLICATION_PASSWORD=your-wordpress-app-password\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Four API integrations, one <code>.env<\/code> file. The built-in <code>loadEnv()<\/code> loads them all automatically.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run build\nnpm run demo \"AI agents for SEO content workflows\"\n<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">2. Project Structure<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>TShandoff\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 miniAgent.ts    # Reusable OpenAI-compatible agent loop (same as before)\n\u2502   \u251c\u2500\u2500 handoff.ts      # Three-agent pipeline: SEO \u2192 writer \u2192 publisher\n\u2502   \u2514\u2500\u2500 demo.ts         # Entry point \u2014 runs the full pipeline\n\u251c\u2500\u2500 dist\/               # Compiled TypeScript output\n\u251c\u2500\u2500 .env.example\n\u251c\u2500\u2500 package.json\n\u2514\u2500\u2500 tsconfig.json\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Three source files, one pipeline. The <code>miniAgent.ts<\/code> is the same <code>MiniAgent<\/code> class from the previous TypeScript article \u2014 the core loop does not change, only the prompts and tools do.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">3. The Core Agent Loop \u2014 A Quick Refresher<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>MiniAgent<\/code> class is identical to the one in the TypeScript agents example. It is a single <code>while<\/code> loop:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async run(userPrompt: string): Promise&lt;string&gt; {\n  this.messages = &#91;\n    { role: \"system\", content: this.systemPrompt },\n    { role: \"user\", content: userPrompt },\n  ];\n\n  for (let iteration = 1; iteration &lt;= this.maxToolIters; iteration++) {\n    const reply = await this._callModel();\n\n    if (reply.tool_calls &amp;&amp; reply.tool_calls.length) {\n      await this._handleToolCalls(reply.tool_calls);\n      continue;  \/\/ loop back to the model\n    }\n\n    return reply.content || \"\";  \/\/ final answer\n  }\n\n  throw new Error(\"No final answer after max iterations.\");\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The call to the model, the tool execution, and the <code>tool_call_id<\/code> protocol are all identical to the earlier TypeScript article. If you have read that, you know this. The difference is in <strong>how we use three instances of this class<\/strong> \u2014 not in the class itself.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">4. The Three Agents<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Each agent is a <code>MiniAgent<\/code> with a different system prompt, a different temperature, and a different set of tools. They do not share state \u2014 the output of one is passed as the input to the next as a plain JSON string.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 1 \u2014 The SEO Researcher<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The SEO agent has one tool: a DataForSEO SERP query that fetches live Google organic results. Its job is to produce a structured <code>ArticleBrief<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export type ArticleBrief = {\n  topic: string;\n  audience: string;\n  angle: string;\n  keyword: string;\n  headings: string&#91;];\n  outline: string&#91;];\n  tone: string;\n  seoNotes: string&#91;];\n  researchContext?: string;\n};\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The system prompt tells it to use the tool before writing, and to return <strong>strict JSON only<\/strong>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const SEO_PROMPT = `\nYou are the SEO agent for a blog workflow.\n\nImportant: before writing the brief, use the DataForSEO research tool\nto inspect live Google organic results. Then use those results to\ninfer search intent, top competitors, and likely article angle.\n\nReturn only valid JSON in this exact structure:\n{\n  \"topic\": \"...\",\n  \"audience\": \"...\",\n  \"angle\": \"...\",\n  \"keyword\": \"...\",\n  \"headings\": &#91;\"...\"],\n  \"outline\": &#91;\"...\"],\n  \"tone\": \"...\",\n  \"seoNotes\": &#91;\"...\"]\n}\n`;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The agent is created with a low temperature (0.4) for consistency:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const seoAgent = buildAgent(SEO_PROMPT, {\n  model: \"gpt-4o-mini\",\n  temperature: 0.4,\n});\n\nseoAgent.addTool(\"dataforseo_google_organic_live_advanced\",\n  \"Search live Google organic results...\",\n  { type: \"object\", properties: { ... }, required: &#91;\"keyword\"] },\n  async (args) =&gt; fetchDataForSeoGoogleOrganicLiveAdvanced({ ... })\n);<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 2 \u2014 The Article Writer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The writer has no tools. It receives the SEO brief plus the scraped research context and produces a complete article draft \u2014 title, summary, outline, content, meta description and slug, all as JSON:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const WRITER_PROMPT = `\nYou are the article writer agent.\n\nYou receive a structured SEO brief plus live SERP and scraped\nsource context. Use those signals to build the article.\n\nReturn valid JSON in this exact structure:\n{\n  \"title\": \"...\",\n  \"summary\": \"...\",\n  \"outline\": &#91;\"...\"],\n  \"content\": \"...\",\n  \"metaDescription\": \"...\",\n  \"slug\": \"...\"\n}\n`;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The writer runs at a higher temperature (0.8) for creative variation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const writerAgent = buildAgent(WRITER_PROMPT, {\n  model: \"gpt-4o-mini\",\n  temperature: 0.8,\n});\n\nconst result = await callModelForJson(writerAgent, `\n  Use this SEO brief and the live SERP\/scrape research to draft the article.\n  SEO brief: ${JSON.stringify(brief, null, 2)}\n  SERP + scraped source research: ${brief.researchContext}\n`);<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 3 \u2014 The WordPress Publisher<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The publisher takes the article draft, the SEO brief and the research context, and formats everything into a WordPress REST API payload. It adds the research-aware framing and ensures the content is ready for the API:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const PUBLISHER_PROMPT = `\nYou are the WordPress publisher agent.\n\nFormat the article draft into a WordPress-ready publish payload\nusing the article plus the live SERP and scraped source context.\n\nReturn valid JSON in this exact structure:\n{\n  \"title\": \"...\",\n  \"content\": \"...\",\n  \"status\": \"draft\",\n  \"slug\": \"...\",\n  \"excerpt\": \"...\",\n  \"meta\": {\n    \"seo_title\": \"...\",\n    \"meta_description\": \"...\"\n  }\n}\n`;<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The publisher runs at a moderate temperature (0.5) \u2014 just enough to frame the research context without rewriting the article.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. The Handoff \u2014 How Data Flows Between Agents<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The handoff is the core idea of this project. It is not complex \u2014 it is a simple chain of async function calls, each passing its output to the next:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function runHandoff(topic: string) {\n  const brief = await seoResearch(topic);        \/\/ step 1: SEO\n  const draft = await writeArticle(brief);        \/\/ step 2: writer\n  const publishPayload = await prepareWordPressPayload(draft, brief); \/\/ step 3: publisher\n  const result = await publishToWordPress(publishPayload);            \/\/ step 4: HTTP\n  return { brief, draft, publishPayload, result };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Each step is a function, not a shared agent context. The data is passed as plain JSON strings, which means:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>No shared memory.<\/strong> Each agent starts with a fresh <code>messages<\/code> array. It cannot see the previous agent&#8217;s conversation.<\/li>\n\n\n\n<li><strong>Clear contract.<\/strong> Each agent returns typed JSON. The next agent receives that JSON and uses it.<\/li>\n\n\n\n<li><strong>Any agent can be replaced.<\/strong> Swap the SEO agent for a different research provider, or the writer for a different model. The rest of the pipeline does not change.<\/li>\n\n\n\n<li><strong>Auditable.<\/strong> Every intermediate output can be inspected, logged, or saved.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Here is what happens in detail:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Step 1: SEO Research (<code>seoResearch()<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The SEO agent is given a topic and a research context. It calls the <code>dataforseo_google_organic_live_advanced<\/code> tool, which:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Sends a POST to the DataForSEO API with the keyword.<\/li>\n\n\n\n<li>Returns the top 20 organic results (title, URL, domain, snippet).<\/li>\n\n\n\n<li>The agent (the model, not the code) decides to use this data.<\/li>\n\n\n\n<li>The <code>gatherSeoResearch()<\/code> function then takes the top 10 URLs and scrapes the top 5 through Jina Reader, returning cleaned plain text.<\/li>\n\n\n\n<li>The agent receives the combined SERP + scraped data and produces the <code>ArticleBrief<\/code> JSON.<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code>async function seoResearch(topic: string): Promise&lt;ArticleBrief&gt; {\n  \/\/ 1. Build the agent with the DataForSEO tool\n  const seoAgent = buildAgent(SEO_PROMPT, { temperature: 0.4 });\n  seoAgent.addTool(\"dataforseo_google_organic_live_advanced\", ..., async (args) =&gt; {\n    return fetchDataForSeoGoogleOrganicLiveAdvanced({ keyword: String(args.keyword) });\n  });\n\n  \/\/ 2. Gather live SERP + scrape top sources\n  const researchContext = await gatherSeoResearch(topic);\n\n  \/\/ 3. Let the agent produce the brief\n  const result = await callModelForJson(seoAgent,\n    `Create a blog SEO brief for: ${topic}. Research: ${researchContext}`\n  );\n\n  return { topic, audience, angle, keyword, headings, outline, tone, seoNotes, researchContext };\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 2: Article Writing (<code>writeArticle()<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The writer receives the brief including the research context. It has no tools \u2014 it only uses the model&#8217;s ability to turn structured data into prose. The full brief and scraped sources are included in the prompt:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function writeArticle(brief: ArticleBrief) {\n  const writerAgent = buildAgent(WRITER_PROMPT, { temperature: 0.8 });\n\n  const result = await callModelForJson(writerAgent, `\n    Use this SEO brief and the live SERP\/scrape research to draft the article.\n    SEO brief: ${JSON.stringify(brief, null, 2)}\n    SERP + scraped source research: ${brief.researchContext ?? \"No research context.\"}\n  `);\n\n  return { title, summary, outline, content, metaDescription, slug };\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 3: WordPress Payload (<code>prepareWordPressPayload()<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The publisher receives the draft, the brief and the research context. It decides how to frame the article for the blog audience, adds a research-aware intro, and produces the exact payload the WordPress REST API expects:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function prepareWordPressPayload(draft, brief) {\n  const publisherAgent = buildAgent(PUBLISHER_PROMPT, { temperature: 0.5 });\n\n  const payloadInput = { draft, brief, researchContext: JSON.parse(brief.researchContext) };\n  const result = await callModelForJson(publisherAgent,\n    `Prepare the WordPress payload: ${JSON.stringify(payloadInput, null, 2)}`\n  );\n\n  return { title, content, status: \"draft\", slug, excerpt, meta };\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Step 4: WordPress HTTP (<code>publishToWordPress()<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This is not an agent \u2014 it is a plain function that sends the payload to the WordPress REST API. No model involved, just <code>fetch()<\/code> with Basic auth:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function publishToWordPress(payload) {\n  const credentials = Buffer.from(`${username}:${password}`).toString(\"base64\");\n  const response = await fetch(\n    `${baseUrl}\/wp-json\/wp\/v2\/posts`,\n    {\n      method: \"POST\",\n      headers: {\n        \"Content-Type\": \"application\/json\",\n        Authorization: `Basic ${credentials}`,\n      },\n      body: JSON.stringify({\n        title: payload.title,\n        content: payload.content,\n        status: payload.status,\n        slug: payload.slug,\n        excerpt: payload.excerpt,\n      }),\n    }\n  );\n\n  const json = await response.json();\n  return { success: true, postId: json.id, slug: json.slug, url: json.link };\n}<\/code><\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">6. The Research Tools<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Two tools power the research phase. Both are plain async functions \u2014 no SDK, no framework.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">DataForSEO SERP query<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The handoff uses a different approach from the <code>MiniAgent<\/code> tool pattern: the <code>fetchDataForSeoGoogleOrganicLiveAdvanced()<\/code> function is called both <em>by<\/em> the agent (as a tool) and <em>outside<\/em> the agent (by <code>gatherSeoResearch()<\/code> to scrape the top URLs). It sends a keyword to the DataForSEO API and returns the top 20 organic results:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function fetchDataForSeoGoogleOrganicLiveAdvanced({\n  keyword, locationCode = 2840, languageCode = \"en\", depth = 100,\n}) {\n  const authHeader = getDataForSeoAuthHeader();\n  const response = await fetch(\n    `${baseUrl}\/v3\/serp\/google\/organic\/live\/advanced`,\n    {\n      method: \"POST\",\n      headers: { Authorization: authHeader, \"Content-Type\": \"application\/json\" },\n      body: JSON.stringify(&#91;{ keyword, location_code: locationCode,\n                              language_code: languageCode, depth }]),\n    }\n  );\n\n  const json = JSON.parse(await response.text());\n  const items = json.tasks?.&#91;0]?.result?.&#91;0]?.items ?? &#91;];\n\n  return JSON.stringify({\n    keyword, total_results: items.length,\n    top_results: items.slice(0, 20).map((item, i) =&gt; ({\n      rank: i + 1, title: item.title, url: item.url,\n      domain: item.domain, snippet: item.description,\n    })),\n  });\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Jina Reader scraping<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Jina Reader (<code>r.jina.ai<\/code>) turns any URL into clean, readable text. It is used to scrape the top 5 ranking pages so the writer can reference real content from the SERP:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function scrapeJinaReader(url: string) {\n  const jinaUrl = `https:\/\/r.jina.ai\/http:\/\/${url.replace(\/^https?:\\\/\\\/\/i, \"\")}`;\n  const headers: Record&lt;string, string&gt; = { accept: \"text\/plain\" };\n\n  const apiKey = process.env.JINA_API_KEY;\n  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;\n\n  const response = await fetch(jinaUrl, { headers });\n  const text = await response.text();\n  return { url, text: text.trim() };\n}<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The scraped content is truncated to 1800 characters per source to keep the context window manageable. The entire research package (SERP + 5 scraped pages) typically fits in ~10K tokens, well within the range of <code>gpt-4o-mini<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">7. Running the Demo<\/h2>\n\n\n\n<pre class=\"wp-block-code\"><code>npm run build\nnode dist\/demo.js \"AI agents for SEO content workflows\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">What happens:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Starting a 3-agent handoff workflow...\n\n--- Step 1: SEO agent calls DataForSEO + scrapes top 5 URLs ---\n--- Step 2: Writer agent produces article draft ---\n--- Step 3: Publisher agent builds WordPress payload ---\n--- Step 4: Publishing to WordPress REST API ---\n\nSEO brief:\n{\n  \"topic\": \"AI agents for SEO content workflows\",\n  \"audience\": \"SEO professionals and content marketers\",\n  \"angle\": \"How AI agents can automate keyword research, content briefs, and SERP analysis\",\n  \"headings\": &#91;\"Introduction\", \"What are AI agents for SEO?\", \"How to build an SEO agent pipeline\", ...],\n  \"seoNotes\": &#91;\"Focus on automation\", \"Include real workflow examples\"]\n}\n\nDraft article:\n{\n  \"title\": \"AI Agents for SEO Content Workflows: A Practical Guide to Automation\",\n  \"summary\": \"Learn how to build a three-agent SEO pipeline...\",\n  \"slug\": \"ai-agents-seo-content-workflows\"\n}\n\nWordPress payload:\n{\n  \"title\": \"AI Agents for SEO Content Workflows: A Practical Guide to Automation\",\n  \"status\": \"draft\",\n  \"slug\": \"ai-agents-seo-content-workflows\"\n}\n\nPublish result:\n{\n  \"success\": true,\n  \"postId\": 1234,\n  \"slug\": \"ai-agents-seo-content-workflows\",\n  \"status\": \"draft\",\n  \"url\": \"https:\/\/your-site.com\/?p=1234\"\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note: the WordPress step requires valid <code>WP_BASE_URL<\/code>, <code>WP_USERNAME<\/code> and <code>WP_APPLICATION_PASSWORD<\/code> in <code>.env<\/code>. Without them, the publish step returns a clean error message instead of crashing.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">8. The Handoff Pattern \u2014 Why It Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The handoff pattern is the most underrated idea in agent development. It solves three problems that single-agent systems struggle with:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 1: Context window limits<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A single agent that researches, writes and publishes would need to hold the entire SERP, the scraped sources, the brief, the draft and the API payload in one conversation. That is 15K+ tokens of context, most of it irrelevant at any given step. A handoff pipeline keeps each agent&#8217;s context focused on its job.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 2: Prompt interference<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When one system prompt covers research, writing and publishing, the instructions compete. The writing rules leak into the research phase, the publishing constraints distract the writer. Separate agents with separate prompts separate the concerns cleanly.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 3: Debugging and iteration<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a pipeline fails, you want to know <em>which<\/em> step failed. A handoff saves every intermediate output. You can inspect the brief, the draft, the payload \u2014 and rerun only the failed step with a fixed prompt, without redoing the entire pipeline.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The handoff is also the most natural pattern for teams: one person researches, another writes, a third publishes. Agents are no different.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">9. Putting It All Together<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Here is the complete checklist to build your own handoff pipeline:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Define your types<\/strong> \u2014 <code>ArticleBrief<\/code>, <code>PublishResult<\/code>, and the intermediate shapes. Each type is the contract between two agents.<\/li>\n\n\n\n<li><strong>Write the prompts<\/strong> \u2014 one system prompt per agent. Each prompt tells the agent exactly what JSON to return. <strong>Strict JSON output is the most important rule.<\/strong><\/li>\n\n\n\n<li><strong>Build the research tools<\/strong> \u2014 DataForSEO for SERP data, Jina Reader for content scraping. Keep them as plain async functions, not agent tools.<\/li>\n\n\n\n<li><strong>Chain the agents<\/strong> \u2014 <code>seoResearch() \u2192 writeArticle() \u2192 prepareWordPressPayload() \u2192 publishToWordPress()<\/code>. Each function calls the next with the previous output.<\/li>\n\n\n\n<li><strong>Validate JSON at every step<\/strong> \u2014 the <code>assertJsonObject()<\/code> helper catches malformed model output before it reaches the next agent.<\/li>\n\n\n\n<li><strong>Audit the outputs<\/strong> \u2014 log every intermediate result. When a pipeline fails, you will know exactly which step produced bad data.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>That is the entire pattern.<\/strong> The handoff is not a framework feature \u2014 it is a chain of async function calls, each with a typed input and a typed output. No shared state, no event bus, no orchestration engine. Just functions, prompts, and a clear boundary between them.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Going Further<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Add a review agent<\/strong> \u2014 insert a quality-check agent between the writer and the publisher. Give it a rubric and ask it to score the draft before publishing.<\/li>\n\n\n\n<li><strong>Swap the research provider<\/strong> \u2014 replace DataForSEO with Google&#8217;s Custom Search API, or replace Jina with your own scraper. The interface never changes: keyword in, structured results out.<\/li>\n\n\n\n<li><strong>Parallel research<\/strong> \u2014 run the SEO agent for multiple keywords in parallel, then merge the briefs before writing. <code>Promise.all()<\/code> is all you need.<\/li>\n\n\n\n<li><strong>Schedule the pipeline<\/strong> \u2014 wrap the handoff in a cron job or a webhook endpoint. The pipeline is a single async function.<\/li>\n\n\n\n<li><strong>Different models per agent<\/strong> \u2014 use <code>gpt-4o<\/code> for the research and writing, <code>gpt-4o-mini<\/code> for the publisher. The <code>MiniAgent<\/code> constructor accepts a per-agent model.<\/li>\n\n\n\n<li><strong>Add images<\/strong> \u2014 the publisher agent could include a DALL-E prompt in the payload, or you could add a fourth agent that generates the featured image.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The full source is at <a href=\"https:\/\/github.com\/juustesout\/typescript-agent-with-handoff-seo-blogger\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/juustesout\/typescript-agent-with-handoff-seo-blogger<\/a>  in the <code>TShandoff<\/code> folder. Clone it, open the files, and trace the handoff from the first line of <code>seoResearch()<\/code> to the last line of <code>publishToWordPress()<\/code> \u2014 it is four functions, and you already understand every one of them.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Basics: build your own SEO Blogger AI Agent in TypeScript<\/p>\n","protected":false},"author":5796,"featured_media":21678,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_sitemap_exclude":false,"_sitemap_priority":"","_sitemap_frequency":"","site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"set","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[543,484,479,305,480,26],"tags":[483],"class_list":["post-21676","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-agents","category-ai","category-javascript","category-programming","category-seo","category-trends","tag-ai"],"_links":{"self":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21676","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/users\/5796"}],"replies":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/comments?post=21676"}],"version-history":[{"count":3,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21676\/revisions"}],"predecessor-version":[{"id":21718,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21676\/revisions\/21718"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media\/21678"}],"wp:attachment":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media?parent=21676"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/categories?post=21676"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/tags?post=21676"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}