{"id":21682,"date":"2026-08-15T15:53:32","date_gmt":"2026-08-15T13:53:32","guid":{"rendered":"https:\/\/www.juust.org\/?p=21682"},"modified":"2026-08-15T22:35:41","modified_gmt":"2026-08-15T20:35:41","slug":"orchestrated-workflow-in-typescript","status":"publish","type":"post","link":"https:\/\/www.juust.org\/index.php\/orchestrated-workflow-in-typescript\/2026\/08\/","title":{"rendered":"Orchestrator Workflow in TypeScript"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\">Research, Write, Review, Publish<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><em>An orchestrator agent that delegates to specialists, reviews the output, and retries on failure \u2014 no frameworks needed.<\/em><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You have seen the single-<a href=\"https:\/\/www.juust.org\/index.php\/tag\/agent\/\" target=\"_blank\" rel=\"noreferrer noopener\">agent<\/a> loop. You have seen the handoff pipeline (agent A \u2192 agent B \u2192 agent C). The next step is the one that real production systems use: an <strong>orchestrator<\/strong> that owns the process, delegates to specialist agents, reviews their output, and only proceeds when quality gates pass.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This guide builds a four-agent orchestrated content pipeline in <strong>vanilla TypeScript<\/strong>:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>SEO Research agent<\/strong> \u2014 queries DataForSEO, scrapes top sources via Jina Reader, produces a structured research brief.<\/li>\n\n\n\n<li><strong>Writer agent<\/strong> \u2014 turns the brief into a complete article draft.<\/li>\n\n\n\n<li><strong>Reviewer agent<\/strong> \u2014 assesses the draft against quality and SEO standards. If it fails, the writer revises.<\/li>\n\n\n\n<li><strong>Publisher agent<\/strong> \u2014 formats the approved article into a WordPress payload and posts it via the REST API.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Everything is on GitHub at https:\/\/github.com\/juustesout\/typescript-agent-orchestrate-workflow <\/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<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Same setup as the handoff example: Node.js 18+, two dev dependencies, a handful of API keys:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cd TSworkflow\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\nDATAFORSEO_API_KEY=your-dataforseo-key\nJINA_API_KEY=your-jina-key\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<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>TSworkflow\/\n\u251c\u2500\u2500 src\/\n\u2502   \u251c\u2500\u2500 miniAgent.ts    # Reusable agent loop (same as before)\n\u2502   \u251c\u2500\u2500 workflow.ts     # Orchestrator + all 4 specialist agents\n\u2502   \u2514\u2500\u2500 demo.ts         # CLI entry point\n\u251c\u2500\u2500 dist\/\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 orchestrator. The <code>miniAgent.ts<\/code> is the same <code>MiniAgent<\/code> class you have seen throughout these articles \u2014 the core loop never changes, only the prompts, tools and orchestration logic 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 Architecture \u2014 What Makes This Different<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The previous handoff example (TShandoff) was a linear chain:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>SEO \u2192 Writer \u2192 Publisher\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This workflow adds two critical elements:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>A reviewer gate<\/strong> \u2014 the writer&#8217;s output is assessed before it reaches the publisher. If the reviewer rejects it, the writer revises it and the reviewer checks again.<\/li>\n\n\n\n<li><strong>Retries on every external call<\/strong> \u2014 DataForSEO, Jina Reader and WordPress HTTP all have built-in retry logic so a transient network failure does not kill the whole pipeline.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">The architecture:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>           \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n           \u2502    Orchestrator      \u2502    \u2190 owns the process\n           \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                     \u2502\n       \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n       \u25bc             \u25bc             \u25bc\n\u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510 \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n\u2502 SEO Research \u2502 \u2502    Writer    \u2502 \u2502   Reviewer   \u2502   \u2190 specialist agents\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518 \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n       \u2502                 \u2502                 \u2502\n       \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                     \u25bc\n             \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n             \u2502  Publisher   \u2502              \u2190 WordPress REST API\n             \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\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\">4. The Orchestrator \u2014 <code>runWorkflow()<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The orchestrator is a single async function that calls the specialist agents in sequence, checks the review, and retries if needed. There is no event bus, no state machine, no workflow engine \u2014 just a <code>while<\/code> loop around the review gate:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>export async function runWorkflow(topic: string) {\n  const research = await runSeoResearch(topic);   \/\/ step 1\n  let draft = await runWriter(research);           \/\/ step 2\n  let review = await runReviewer(draft);           \/\/ step 3\n\n  \/\/ Review-retry loop\n  let attempts = 1;\n  while (!review.approved &amp;&amp; attempts &lt; REVIEW_MAX_ATTEMPTS) {\n    draft = await runWriterWithFeedback(research, draft, review);  \/\/ revise\n    review = await runReviewer(draft);                              \/\/ re-review\n    attempts++;\n  }\n\n  if (!review.approved) {\n    throw new Error(\"Review rejected after max attempts.\");\n  }\n\n  const publishPayload = await runPublisher(draft);  \/\/ step 4\n  const publishResult = await publishToWordPress(publishPayload);  \/\/ step 5\n  return { research, draft, review, publishPayload, publishResult };\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The orchestrator does four things that a simple handoff does not:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Owns the retry decision<\/strong> \u2014 if the writer produces a weak draft, the orchestrator does not publish it; it sends it back.<\/li>\n\n\n\n<li><strong>Logs every intermediate state<\/strong> \u2014 research, draft, review, payload and publish result are all available. Debugging is trivial.<\/li>\n\n\n\n<li><strong>Throws on permanent failure<\/strong> \u2014 if the review keeps failing or the publisher keeps erroring, the orchestrator throws with a clear message rather than silently returning bad data.<\/li>\n\n\n\n<li><strong>Sets different temperatures per agent<\/strong> \u2014 research and review run cold (0.4) for consistency; the writer runs warmer (0.8) for creativity; the publisher runs moderate (0.4).<\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">5. The Four Specialist Agents<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 1: SEO Research<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The SEO agent uses one tool (DataForSEO SERP query) and receives the scraped research context before writing its brief. Its output defines the angle, audience, intent and suggested headings:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>type ResearchSummary = {\n  keyword: string;\n  topResults: Array&lt;{ rank: number; title: string; url: string; snippet: string }&gt;;\n  scrapedSources: Array&lt;{ rank: number; url: string; title: string; content: string }&gt;;\n};\n\nconst SEO_PROMPT = `\nYou are the SEO research agent.\nProduce a research brief using live SERP data and top source text.\nReturn valid JSON only:\n{\n  \"keyword\": \"...\",\n  \"angle\": \"...\",\n  \"audience\": \"...\",\n  \"searchIntent\": \"...\",\n  \"seoNotes\": &#91;\"...\"],\n  \"headings\": &#91;\"...\"]\n}\n`;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The research phase has its own <code>gatherResearch()<\/code> function that runs DataForSEO and Jina with retry. The agent receives the combined result and produces the brief:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>async function runSeoResearch(topic: string) {\n  const agent = buildAgent(SEO_PROMPT, { temperature: 0.4 });\n  const research = await gatherResearch(topic);  \/\/ DataForSEO + Jina + retries\n  const result = await callModelForJson(\n    agent,\n    `Create an SEO brief for: ${topic}. Research: ${JSON.stringify(research)}`\n  );\n  return { keyword, angle, audience, searchIntent, seoNotes, headings, research };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 2: Writer<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The writer receives the research (brief + live SERP + scraped sources) and produces a draft article. It runs at a higher temperature (0.8) for stylistic variation:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const WRITER_PROMPT = `\nYou are the article writer agent.\nUse the research brief and live source context to write a complete article.\nReturn valid JSON only:\n{\n  \"title\": \"...\", \"summary\": \"...\", \"outline\": &#91;\"...\"],\n  \"content\": \"...\", \"metaDescription\": \"...\", \"slug\": \"...\"\n}\n`;\n\nasync function runWriter(research) {\n  const agent = buildAgent(WRITER_PROMPT, { temperature: 0.8 });\n  const result = await callModelForJson(\n    agent,\n    `Write the article using this brief: ${JSON.stringify(research)}`\n  );\n  return { title, summary, outline, content, metaDescription, slug };\n}\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 3: Reviewer (the quality gate)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The reviewer is the new agent in this workflow. It reads the draft and returns a structured assessment:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>type ReviewResult = {\n  approved: boolean;\n  score: number;\n  issues: string&#91;];\n  requiredChanges: string&#91;];\n  summary: string;\n};\n\nconst REVIEWER_PROMPT = `\nYou are the reviewer agent.\nReview the article against SEO and quality standards.\nReturn valid JSON only:\n{\n  \"approved\": true\/false,\n  \"score\": 0-100,\n  \"issues\": &#91;\"...\"],\n  \"requiredChanges\": &#91;\"...\"],\n  \"summary\": \"...\"\n}\n`;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The reviewer runs cold (0.4) for consistent judgement. If <code>approved<\/code> is <code>false<\/code>, the orchestrator sends the draft back to the writer with the reviewer&#8217;s <code>issues<\/code> and <code>requiredChanges<\/code> as feedback:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const revisionPrompt = `\n  The draft has been rejected by the reviewer.\n  Use the review issues and required changes to rewrite the article.\n  Research context: ${JSON.stringify(research)}\n  Existing draft: ${JSON.stringify(draft)}\n  Review result: ${JSON.stringify(review)}\n`;\nconst revised = await callModelForJson(revisionAgent, revisionPrompt);\ndraft = { title, summary, outline, content, metaDescription, slug };\nreview = await runReviewer(draft);\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Agent 4: Publisher<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The publisher formats the approved article into a WordPress REST API payload:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const PUBLISHER_PROMPT = `\nYou are the publication agent.\nTurn a reviewed article into a WordPress-ready payload.\nReturn valid JSON only:\n{\n  \"title\": \"...\", \"content\": \"...\", \"status\": \"draft\",\n  \"slug\": \"...\", \"excerpt\": \"...\",\n  \"meta\": { \"seo_title\": \"...\", \"meta_description\": \"...\" }\n}\n`;\n\nasync function runPublisher(article: DraftArticle) {\n  const agent = buildAgent(PUBLISHER_PROMPT, { temperature: 0.4 });\n  const result = await callModelForJson(\n    agent,\n    `Create the WordPress payload: ${JSON.stringify(article)}`\n  );\n  return { title, content, status, slug, excerpt, meta };\n}\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. Retries \u2014 Making the Pipeline Resilient<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">External API calls fail. Networks time out. Services return empty responses. The workflow handles this with retry wrappers around every external dependency:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>const MAX_RETRIES = 1;  \/\/ one initial attempt + one retry = 2 total\n\n\/\/ DataForSEO \u2014 retry on empty or malformed SERP\nasync function fetchDataForSeoWithRetry({ keyword, depth }) {\n  let lastResult = \"\";\n  for (let attempt = 0; attempt &lt;= MAX_RETRIES; attempt++) {\n    lastResult = await fetchDataForSeoGoogleOrganicLiveAdvanced({ keyword, depth });\n    try {\n      const parsed = JSON.parse(lastResult);\n      if (parsed.topResults?.length &gt; 0) return lastResult;\n    } catch { \/* retry *\/ }\n  }\n  throw new Error(`DataForSEO failed after ${MAX_RETRIES + 1} attempts.`);\n}\n\n\/\/ Jina Reader \u2014 retry on empty or error\nasync function scrapeJinaReaderWithRetry(url: string) {\n  for (let attempt = 0; attempt &lt;= MAX_RETRIES; attempt++) {\n    const result = await scrapeJinaReader(url);\n    if (result.text &amp;&amp; !result.error) return result;\n  }\n  return { url, text: \"\", error: \"Jina scrape unavailable.\" };\n}\n\n\/\/ WordPress publish \u2014 retry on HTTP or network error\nfor (let attempt = 0; attempt &lt;= MAX_RETRIES; attempt++) {\n  try {\n    const response = await fetch(url, { ... });\n    if (response.ok) return { success: true, ... };\n    lastResult = { success: false, message: \"...\" };\n  } catch (error) {\n    lastResult = { success: false, message: \"...\" };\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The workflow also throws early when critical data is missing:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>if (urls.length === 0) {\n  throw new Error(\"No valid SERP URLs returned after retry policy.\");\n}\nif (usableSources.length === 0) {\n  throw new Error(\"Jina scrapes failed for all sources after retry policy.\");\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">This means a pipeline run either succeeds completely or fails with a clear, actionable error \u2014 never silently continues with empty data.<\/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\">The full output shows every stage:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Starting orchestrated content workflow...\n\nResearch: {\n  \"keyword\": \"test orchestrator\",\n  \"angle\": \"Understanding Test Orchestration in Modern QA\",\n  \"audience\": \"QA professionals, software developers\",\n  \"headings\": &#91;\"What is Test Orchestration?\", \"Benefits\", \"Implementation\", \"Role of AI\", \"Challenges\"]\n}\n\nDraft article: { \"title\": \"Understanding Test Orchestration...\",\n  \"summary\": \"Test orchestration is essential for streamlining QA...\",\n  \"slug\": \"importance-of-test-orchestration-in-qa-processes\" }\n\nReview result: { \"approved\": true, \"score\": 92,\n  \"summary\": \"Well-structured, informative, meets SEO standards.\" }\n\nWordPress payload: { \"title\": \"...\", \"status\": \"draft\",\n  \"slug\": \"importance-of-test-orchestration-in-qa-processes\" }\n\nPublish result: { \"success\": true, \"postId\": 1583,\n  \"url\": \"https:\/\/your-site.com\/?p=1583\" }\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Note: the reviewer scores the article 92\/100 with no issues and approves it in one pass. If the score were lower, the orchestrator would loop the draft back to the writer with the reviewer&#8217;s feedback before publishing.<\/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 Orchestrator Pattern \u2014 Why It Matters<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The orchestrator pattern solves problems that neither a single agent nor a simple handoff can:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 1: Quality control<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A handoff pipeline trusts every agent to produce good output. An orchestrator does not \u2014 it adds a reviewer gate that catches weak drafts before they reach the publisher. In a real workflow, the reviewer might reject 20-30% of drafts on the first pass, forcing the writer to revise with specific feedback.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 2: Failure isolation<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When a handoff step fails, the whole pipeline fails. The orchestrator wraps every external call with retry logic, so a transient network error (common with DataForSEO and Jina) does not kill the pipeline. Only permanent failures \u2014 no SERP results after retries, all Jina scrapes failing \u2014 cause a clean throw.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 3: Flexible process<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The orchestrator owns the loop, not the agents. You can add a second reviewer, insert an image generation agent, or skip the review gate for simple topics \u2014 all by changing the orchestrator function, not the agents themselves. The agents are stateless functions with typed inputs and outputs; the orchestrator is the process.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Problem 4: Observability<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Every intermediate value \u2014 research, draft, review, payload, publish result \u2014 is returned by the orchestrator. You can log it, save it, or serve it via a webhook. Debugging a failed run is a matter of inspecting one JSON object.<\/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 checklist to build your own orchestrated workflow:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Define your types<\/strong> \u2014 <code>ResearchSummary<\/code>, <code>DraftArticle<\/code>, <code>ReviewResult<\/code>, <code>PublishResult<\/code>. Every agent has a typed input and a typed output.<\/li>\n\n\n\n<li><strong>Write the prompts<\/strong> \u2014 one per agent. Every prompt demands <strong>strict JSON output<\/strong> with the <code>Do not add markdown fences<\/code> rule (a common model failure mode).<\/li>\n\n\n\n<li><strong>Build retry wrappers<\/strong> \u2014 every external API call (DataForSEO, Jina, WordPress) gets a retry loop. <code>MAX_RETRIES = 1<\/code> is a good default: one initial attempt plus one retry.<\/li>\n\n\n\n<li><strong>Implement the orchestrator<\/strong> \u2014 a single async function that calls the agents in sequence, checks the review, and retries the writer on rejection. That is it.<\/li>\n\n\n\n<li><strong>Throw on permanent failure<\/strong> \u2014 if critical data is missing after retries, or the review never passes, throw with a clear message. Do not silently publish bad data.<\/li>\n\n\n\n<li><strong>Return everything<\/strong> \u2014 the orchestrator returns research, draft, review, payload and publish result. Observability is built in by design.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>That is the entire pattern.<\/strong> The orchestrator is not a framework \u2014 it is a function that calls other functions, retries when they fail, and checks the output before proceeding. Four specialist agents, one control loop, zero dependencies beyond the OpenAI API.<\/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>Multiple review gates<\/strong> \u2014 add a second reviewer (e.g. a style checker and a factual accuracy checker) that both must approve.<\/li>\n\n\n\n<li><strong>Parallel agents<\/strong> \u2014 run the SEO agent for multiple keywords in parallel with <code>Promise.all()<\/code>, then merge the results.<\/li>\n\n\n\n<li><strong>Human-in-the-loop<\/strong> \u2014 instead of an AI reviewer, pause the workflow and send a Slack message asking a human to approve the draft. The rest of the pipeline stays the same.<\/li>\n\n\n\n<li><strong>Different models per agent<\/strong> \u2014 use <code>gpt-4o<\/code> for the reviewer (stricter), <code>gpt-4o-mini<\/code> for the writer (cheaper, more creative).<\/li>\n\n\n\n<li><strong>Schedule the orchestrator<\/strong> \u2014 wrap <code>runWorkflow()<\/code> in a cron job or a webhook endpoint. One async function, one endpoint.<\/li>\n\n\n\n<li><strong>Add image generation<\/strong> \u2014 insert a DALL-E agent between the writer and the publisher that generates a featured image from the article title.<\/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-orchestrate-workflow\" target=\"_blank\" rel=\"noopener\">https:\/\/github.com\/juustesout\/typescript-agent-orchestrate-workflow<\/a>. Open <code>workflow.ts<\/code> and trace the orchestrator from <code>runWorkflow()<\/code> to each specialist agent \u2014 it is five async functions, and you already understand every one of them.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>An Orchestrator Agent pattern in TypeScript (with repo)<\/p>\n","protected":false},"author":5796,"featured_media":21700,"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],"tags":[483],"class_list":["post-21682","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-agents","category-ai","category-javascript","category-programming","tag-ai"],"_links":{"self":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21682","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=21682"}],"version-history":[{"count":3,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21682\/revisions"}],"predecessor-version":[{"id":21719,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/posts\/21682\/revisions\/21719"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media\/21700"}],"wp:attachment":[{"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/media?parent=21682"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/categories?post=21682"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.juust.org\/index.php\/wp-json\/wp\/v2\/tags?post=21682"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}