Human-in-the-Loop

medium15 minhuman-in-loop.ts

Nothing gets published until an editor approves the draft the agent wrote.

What it shows

  • Add an approval step with humanWorkflowNode(approvalNode(...))
  • Answer it from outside the run through the ApprovalStore — like an API handler or UI would
  • Branch on the decision

The code

human-in-loop.ts
import { Agent, Cogitator } from '@cogitator-ai/core';
import {
  InMemoryApprovalStore,
  WorkflowBuilder,
  WorkflowExecutor,
  agentNode,
  approvalNode,
  humanWorkflowNode,
} from '@cogitator-ai/workflows';

const apiKey = process.env.GOOGLE_API_KEY;
if (!apiKey) throw new Error('Set GOOGLE_API_KEY');

interface PublishingState {
  [key: string]: unknown;
  topic: string;
  draft: string;
  approved: boolean;
  status: string;
}

const cog = new Cogitator({ llm: { providers: { google: { apiKey } } } });
const approvalStore = new InMemoryApprovalStore();

const writer = new Agent({
  name: 'content-writer',
  model: 'google/gemini-3.5-flash-lite',
  instructions: 'You are a technical writer. Write a blog post under 150 words with a title.',
  temperature: 0.7,
});

const write = agentNode<PublishingState>(writer, {
  inputMapper: (state) => `Write a short blog post about: ${state.topic}`,
  stateMapper: (result) => ({ draft: result.output }),
});

const review = humanWorkflowNode(
  approvalNode<PublishingState>('editorial-review', {
    title: 'Editorial review',
    description: (state) => `Publish this draft?\n\n${state.draft}`,
    assignee: 'editor@company.com',
    timeout: 60 * 60 * 1000,
    timeoutAction: 'reject',
  }),
  { stateMapper: (result) => ({ approved: result.approved }) }
);

const workflow = new WorkflowBuilder<PublishingState>('content-publishing')
  .initialState({ topic: '', draft: '', approved: false, status: 'draft' })
  .addNode(write.name, write.fn)
  .addNode(review.name, review.fn, { after: [write.name] })
  .addConditional('decide', (state) => (state.approved ? 'publish' : 'archive'), {
    after: [review.name],
  })
  .addNode('publish', async () => ({ state: { status: 'published' } }), { after: ['decide'] })
  .addNode('archive', async () => ({ state: { status: 'rejected' } }), { after: ['decide'] })
  .entryPoint(write.name)
  .build();

const run = new WorkflowExecutor(cog).execute(
  workflow,
  { topic: 'Building type-safe AI agents with TypeScript' },
  { approvalStore }
);

async function nextPendingRequest() {
  for (;;) {
    const [request] = await approvalStore.getPendingRequests();
    if (request) return request;
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
}

const request = await nextPendingRequest();
console.log(`${request.title} for ${request.assignee}:\n${request.description}`);

await approvalStore.submitResponse({
  requestId: request.id,
  decision: true,
  respondedBy: 'editor@company.com',
  respondedAt: Date.now(),
  comment: 'Ship it',
});

const result = await run;
console.log('Status:', result.state.status);

approvalStore.dispose();
await cog.close();

Run it

install · bash
pnpm add @cogitator-ai/core @cogitator-ai/workflows
EnvironmentGOOGLE_API_KEY
run · bash
GOOGLE_API_KEY=your-key npx tsx human-in-loop.ts

Or run the full example from a clone of the repository (keys in .env):

repository · bash
npx tsx examples/workflows/02-human-in-loop.ts

Learn more