Orchestrator Workflow in TypeScript

orchestrator ai agent pattern

Research, Write, Review, Publish

An orchestrator agent that delegates to specialists, reviews the output, and retries on failure — no frameworks needed.

You have seen the single-agent loop. You have seen the handoff pipeline (agent A → agent B → agent C). The next step is the one that real production systems use: an orchestrator that owns the process, delegates to specialist agents, reviews their output, and only proceeds when quality gates pass.

This guide builds a four-agent orchestrated content pipeline in vanilla TypeScript:

  1. SEO Research agent — queries DataForSEO, scrapes top sources via Jina Reader, produces a structured research brief.
  2. Writer agent — turns the brief into a complete article draft.
  3. Reviewer agent — assesses the draft against quality and SEO standards. If it fails, the writer revises.
  4. Publisher agent — formats the approved article into a WordPress payload and posts it via the REST API.

Everything is on GitHub at https://github.com/juustesout/typescript-agent-orchestrate-workflow


1. Installatie

Same setup as the handoff example: Node.js 18+, two dev dependencies, a handful of API keys:

cd TSworkflow
npm install
cp .env.example .env

Open .env and add your keys:

OPENAI_API_KEY=sk-proj-your-key-here
DATAFORSEO_API_KEY=your-dataforseo-key
JINA_API_KEY=your-jina-key
WP_BASE_URL=https://your-site.com
WP_USERNAME=your-wordpress-username
WP_APPLICATION_PASSWORD=your-wordpress-app-password
npm run build
npm run demo "AI agents for SEO content workflows"

2. Project Structure

TSworkflow/
├── src/
│   ├── miniAgent.ts    # Reusable agent loop (same as before)
│   ├── workflow.ts     # Orchestrator + all 4 specialist agents
│   └── demo.ts         # CLI entry point
├── dist/
├── .env.example
├── package.json
└── tsconfig.json

Three source files, one orchestrator. The miniAgent.ts is the same MiniAgent class you have seen throughout these articles — the core loop never changes, only the prompts, tools and orchestration logic do.


3. The Architecture — What Makes This Different

The previous handoff example (TShandoff) was a linear chain:

SEO → Writer → Publisher

This workflow adds two critical elements:

  • A reviewer gate — the writer’s output is assessed before it reaches the publisher. If the reviewer rejects it, the writer revises it and the reviewer checks again.
  • Retries on every external call — DataForSEO, Jina Reader and WordPress HTTP all have built-in retry logic so a transient network failure does not kill the whole pipeline.

The architecture:

           ┌──────────────────────┐
           │    Orchestrator      │    ← owns the process
           └─────────┬────────────┘
                     │
       ┌─────────────┼─────────────┐
       ▼             ▼             ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ SEO Research │ │    Writer    │ │   Reviewer   │   ← specialist agents
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘
       │                 │                 │
       └─────────────┬───┴─────────────────┘
                     ▼
             ┌──────────────┐
             │  Publisher   │              ← WordPress REST API
             └──────────────┘

4. The Orchestrator — runWorkflow()

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 — just a while loop around the review gate:

export async function runWorkflow(topic: string) {
  const research = await runSeoResearch(topic);   // step 1
  let draft = await runWriter(research);           // step 2
  let review = await runReviewer(draft);           // step 3

  // Review-retry loop
  let attempts = 1;
  while (!review.approved && attempts < REVIEW_MAX_ATTEMPTS) {
    draft = await runWriterWithFeedback(research, draft, review);  // revise
    review = await runReviewer(draft);                              // re-review
    attempts++;
  }

  if (!review.approved) {
    throw new Error("Review rejected after max attempts.");
  }

  const publishPayload = await runPublisher(draft);  // step 4
  const publishResult = await publishToWordPress(publishPayload);  // step 5
  return { research, draft, review, publishPayload, publishResult };
}

The orchestrator does four things that a simple handoff does not:

  1. Owns the retry decision — if the writer produces a weak draft, the orchestrator does not publish it; it sends it back.
  2. Logs every intermediate state — research, draft, review, payload and publish result are all available. Debugging is trivial.
  3. Throws on permanent failure — if the review keeps failing or the publisher keeps erroring, the orchestrator throws with a clear message rather than silently returning bad data.
  4. Sets different temperatures per agent — research and review run cold (0.4) for consistency; the writer runs warmer (0.8) for creativity; the publisher runs moderate (0.4).

5. The Four Specialist Agents

Agent 1: SEO Research

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:

type ResearchSummary = {
  keyword: string;
  topResults: Array<{ rank: number; title: string; url: string; snippet: string }>;
  scrapedSources: Array<{ rank: number; url: string; title: string; content: string }>;
};

const SEO_PROMPT = `
You are the SEO research agent.
Produce a research brief using live SERP data and top source text.
Return valid JSON only:
{
  "keyword": "...",
  "angle": "...",
  "audience": "...",
  "searchIntent": "...",
  "seoNotes": ["..."],
  "headings": ["..."]
}
`;

The research phase has its own gatherResearch() function that runs DataForSEO and Jina with retry. The agent receives the combined result and produces the brief:

async function runSeoResearch(topic: string) {
  const agent = buildAgent(SEO_PROMPT, { temperature: 0.4 });
  const research = await gatherResearch(topic);  // DataForSEO + Jina + retries
  const result = await callModelForJson(
    agent,
    `Create an SEO brief for: ${topic}. Research: ${JSON.stringify(research)}`
  );
  return { keyword, angle, audience, searchIntent, seoNotes, headings, research };
}

Agent 2: Writer

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:

const WRITER_PROMPT = `
You are the article writer agent.
Use the research brief and live source context to write a complete article.
Return valid JSON only:
{
  "title": "...", "summary": "...", "outline": ["..."],
  "content": "...", "metaDescription": "...", "slug": "..."
}
`;

async function runWriter(research) {
  const agent = buildAgent(WRITER_PROMPT, { temperature: 0.8 });
  const result = await callModelForJson(
    agent,
    `Write the article using this brief: ${JSON.stringify(research)}`
  );
  return { title, summary, outline, content, metaDescription, slug };
}

Agent 3: Reviewer (the quality gate)

The reviewer is the new agent in this workflow. It reads the draft and returns a structured assessment:

type ReviewResult = {
  approved: boolean;
  score: number;
  issues: string[];
  requiredChanges: string[];
  summary: string;
};

const REVIEWER_PROMPT = `
You are the reviewer agent.
Review the article against SEO and quality standards.
Return valid JSON only:
{
  "approved": true/false,
  "score": 0-100,
  "issues": ["..."],
  "requiredChanges": ["..."],
  "summary": "..."
}
`;

The reviewer runs cold (0.4) for consistent judgement. If approved is false, the orchestrator sends the draft back to the writer with the reviewer’s issues and requiredChanges as feedback:

const revisionPrompt = `
  The draft has been rejected by the reviewer.
  Use the review issues and required changes to rewrite the article.
  Research context: ${JSON.stringify(research)}
  Existing draft: ${JSON.stringify(draft)}
  Review result: ${JSON.stringify(review)}
`;
const revised = await callModelForJson(revisionAgent, revisionPrompt);
draft = { title, summary, outline, content, metaDescription, slug };
review = await runReviewer(draft);

Agent 4: Publisher

The publisher formats the approved article into a WordPress REST API payload:

const PUBLISHER_PROMPT = `
You are the publication agent.
Turn a reviewed article into a WordPress-ready payload.
Return valid JSON only:
{
  "title": "...", "content": "...", "status": "draft",
  "slug": "...", "excerpt": "...",
  "meta": { "seo_title": "...", "meta_description": "..." }
}
`;

async function runPublisher(article: DraftArticle) {
  const agent = buildAgent(PUBLISHER_PROMPT, { temperature: 0.4 });
  const result = await callModelForJson(
    agent,
    `Create the WordPress payload: ${JSON.stringify(article)}`
  );
  return { title, content, status, slug, excerpt, meta };
}

6. Retries — Making the Pipeline Resilient

External API calls fail. Networks time out. Services return empty responses. The workflow handles this with retry wrappers around every external dependency:

const MAX_RETRIES = 1;  // one initial attempt + one retry = 2 total

// DataForSEO — retry on empty or malformed SERP
async function fetchDataForSeoWithRetry({ keyword, depth }) {
  let lastResult = "";
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    lastResult = await fetchDataForSeoGoogleOrganicLiveAdvanced({ keyword, depth });
    try {
      const parsed = JSON.parse(lastResult);
      if (parsed.topResults?.length > 0) return lastResult;
    } catch { /* retry */ }
  }
  throw new Error(`DataForSEO failed after ${MAX_RETRIES + 1} attempts.`);
}

// Jina Reader — retry on empty or error
async function scrapeJinaReaderWithRetry(url: string) {
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
    const result = await scrapeJinaReader(url);
    if (result.text && !result.error) return result;
  }
  return { url, text: "", error: "Jina scrape unavailable." };
}

// WordPress publish — retry on HTTP or network error
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
  try {
    const response = await fetch(url, { ... });
    if (response.ok) return { success: true, ... };
    lastResult = { success: false, message: "..." };
  } catch (error) {
    lastResult = { success: false, message: "..." };
  }
}

The workflow also throws early when critical data is missing:

if (urls.length === 0) {
  throw new Error("No valid SERP URLs returned after retry policy.");
}
if (usableSources.length === 0) {
  throw new Error("Jina scrapes failed for all sources after retry policy.");
}

This means a pipeline run either succeeds completely or fails with a clear, actionable error — never silently continues with empty data.


7. Running the Demo

npm run build
node dist/demo.js "AI agents for SEO content workflows"

The full output shows every stage:

Starting orchestrated content workflow...

Research: {
  "keyword": "test orchestrator",
  "angle": "Understanding Test Orchestration in Modern QA",
  "audience": "QA professionals, software developers",
  "headings": ["What is Test Orchestration?", "Benefits", "Implementation", "Role of AI", "Challenges"]
}

Draft article: { "title": "Understanding Test Orchestration...",
  "summary": "Test orchestration is essential for streamlining QA...",
  "slug": "importance-of-test-orchestration-in-qa-processes" }

Review result: { "approved": true, "score": 92,
  "summary": "Well-structured, informative, meets SEO standards." }

WordPress payload: { "title": "...", "status": "draft",
  "slug": "importance-of-test-orchestration-in-qa-processes" }

Publish result: { "success": true, "postId": 1583,
  "url": "https://your-site.com/?p=1583" }

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’s feedback before publishing.


8. The Orchestrator Pattern — Why It Matters

The orchestrator pattern solves problems that neither a single agent nor a simple handoff can:

Problem 1: Quality control

A handoff pipeline trusts every agent to produce good output. An orchestrator does not — 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.

Problem 2: Failure isolation

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 — no SERP results after retries, all Jina scrapes failing — cause a clean throw.

Problem 3: Flexible process

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 — 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.

Problem 4: Observability

Every intermediate value — research, draft, review, payload, publish result — 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.


9. Putting It All Together

Here is the checklist to build your own orchestrated workflow:

  1. Define your typesResearchSummary, DraftArticle, ReviewResult, PublishResult. Every agent has a typed input and a typed output.
  2. Write the prompts — one per agent. Every prompt demands strict JSON output with the Do not add markdown fences rule (a common model failure mode).
  3. Build retry wrappers — every external API call (DataForSEO, Jina, WordPress) gets a retry loop. MAX_RETRIES = 1 is a good default: one initial attempt plus one retry.
  4. Implement the orchestrator — a single async function that calls the agents in sequence, checks the review, and retries the writer on rejection. That is it.
  5. Throw on permanent failure — if critical data is missing after retries, or the review never passes, throw with a clear message. Do not silently publish bad data.
  6. Return everything — the orchestrator returns research, draft, review, payload and publish result. Observability is built in by design.

That is the entire pattern. The orchestrator is not a framework — 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.


Going Further

  • Multiple review gates — add a second reviewer (e.g. a style checker and a factual accuracy checker) that both must approve.
  • Parallel agents — run the SEO agent for multiple keywords in parallel with Promise.all(), then merge the results.
  • Human-in-the-loop — 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.
  • Different models per agent — use gpt-4o for the reviewer (stricter), gpt-4o-mini for the writer (cheaper, more creative).
  • Schedule the orchestrator — wrap runWorkflow() in a cron job or a webhook endpoint. One async function, one endpoint.
  • Add image generation — insert a DALL-E agent between the writer and the publisher that generates a featured image from the article title.

The full source is at https://github.com/juustesout/typescript-agent-orchestrate-workflow. Open workflow.ts and trace the orchestrator from runWorkflow() to each specialist agent — it is five async functions, and you already understand every one of them.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top