Skip to Content
How-To GuidesTypeScriptParallel Workers — Fan-Out / Fan-In (TypeScript)

Parallel Workers — Fan-Out / Fan-In (TypeScript)

Overview

Golem agents process invocations sequentially — a single agent cannot run work in parallel. To execute work concurrently, distribute it across multiple agent instances. This skill covers two approaches:

  1. Child agents via AgentClass.get(id) — spawn separate agent instances, dispatch work, and collect results
  2. fork() — clone the current agent at the current execution point for lightweight parallel execution

Approach 1: Child Agent Fan-Out

Spawn child agents, call them concurrently with Promise.all, and aggregate results.

Basic Pattern

import { BaseAgent, agent } from '@golemcloud/golem-ts-sdk'; @agent() class Coordinator extends BaseAgent { constructor() { super(); } async fanOut(items: string[]): Promise<string[]> { // Spawn one child per item and call concurrently const promises = items.map(async (item, i) => { const child = Worker.get(i); return await child.process(item); }); // Wait for all children to finish return await Promise.all(promises); } } @agent() class Worker extends BaseAgent { private readonly id: number; constructor(id: number) { super(); this.id = id; } async process(data: string): Promise<string> { return `processed-${data}`; } }

Chunked Fan-Out

When spawning many children, batch them to limit concurrency:

async fanOutChunked(ids: number[]): Promise<number[]> { const chunks = arrayChunks(ids, 5); // Process 5 at a time const results: number[] = []; for (const chunk of chunks) { const promises = chunk.map(async id => { return await Worker.get(id).compute(id); }); results.push(...await Promise.all(promises)); } return results; } function arrayChunks<T>(arr: T[], size: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < arr.length; i += size) { chunks.push(arr.slice(i, i + size)); } return chunks; }

Fire-and-Forget with Promise Collection

For long-running work, trigger children with fire-and-forget and collect results via Golem promises:

import { BaseAgent, agent, createPromise, awaitPromise, completePromise, PromiseId, } from '@golemcloud/golem-ts-sdk'; @agent() class Coordinator extends BaseAgent { constructor() { super(); } async dispatchAndCollect(regions: string[]): Promise<string[]> { // Create one promise per child const promiseIds = regions.map(() => createPromise()); // Fire-and-forget: trigger each child with its promise ID regions.forEach((region, i) => { RegionWorker.get(region).runReport.trigger(promiseIds[i]); }); // Collect all results (agent suspends until each promise completes) const results = await Promise.all( promiseIds.map(async pid => { const bytes = await awaitPromise(pid); return new TextDecoder().decode(bytes); }) ); return results; } } @agent() class RegionWorker extends BaseAgent { private readonly region: string; constructor(region: string) { super(); this.region = region; } async runReport(promiseId: PromiseId): Promise<void> { const report = `Report for ${this.region}: OK`; completePromise(promiseId, new TextEncoder().encode(report)); } }

Error Handling

Use Promise.allSettled to handle partial failures:

async fanOutWithErrors(items: string[]): Promise<{ successes: string[]; failures: string[] }> { const promises = items.map(async (item, i) => { const child = Worker.get(i); return await child.process(item); }); const settled = await Promise.allSettled(promises); const successes: string[] = []; const failures: string[] = []; settled.forEach((result, i) => { if (result.status === 'fulfilled') { successes.push(result.value); } else { failures.push(`Item ${items[i]} failed: ${result.reason}`); } }); return { successes, failures }; }

Approach 2: fork()

fork() clones the current agent at the current execution point, creating a new agent instance with the same state but a unique phantom ID. Use Golem promises to synchronize between the original and forked agents.

Basic Fork Pattern

import { BaseAgent, agent, fork, createPromise, awaitPromise, completePromise, } from '@golemcloud/golem-ts-sdk'; @agent() class ForkAgent extends BaseAgent { constructor() { super(); } async parallelCompute(): Promise<string> { const promiseId = createPromise(); const result = fork(); switch (result.tag) { case 'original': // Wait for the forked agent to complete the promise const bytes = await awaitPromise(promiseId); const forkedResult = new TextDecoder().decode(bytes); return `Combined: original + ${forkedResult}`; case 'forked': // Do work in the forked copy const computed = "forked-result"; completePromise(promiseId, new TextEncoder().encode(computed)); return "forked done"; // This return is only seen by the forked agent } } }

Multi-Fork Fan-Out

Fork multiple times for N-way parallelism:

async multiFork(n: number): Promise<string[]> { const promiseIds = Array.from({ length: n }, () => createPromise()); for (let i = 0; i < n; i++) { const result = fork(); if (result.tag === 'forked') { // Each forked agent does its slice of work const output = `result-from-fork-${i}`; completePromise(promiseIds[i], new TextEncoder().encode(output)); return []; // Forked agent exits here } } // Original agent collects all results const results = await Promise.all( promiseIds.map(async pid => { const bytes = await awaitPromise(pid); return new TextDecoder().decode(bytes); }) ); return results; }

When to Use Which Approach

CriteriaChild Agentsfork()
Work is independent and stateless✅ Best fitWorks but overkill
Need to share current state with workers❌ Must pass via args✅ Forked copy inherits state
Workers need persistent identity✅ Each has own ID❌ Forked agents are ephemeral phantoms
Number of parallel tasks is dynamic✅ Spawn as many as needed✅ Fork in a loop
Need simple error isolation✅ Child failure doesn’t crash parent⚠️ Forked agent shares oplog lineage

Key Points

  • No threads: Golem is single-threaded per agent — parallelism is achieved by distributing across agent instances
  • Durability: All RPC calls, promises, and fork operations are durably recorded — work survives crashes
  • Deadlock avoidance: Never have two agents awaiting each other synchronously — use .trigger() to break cycles
  • Cleanup: Child agents persist after the coordinator finishes; delete them explicitly if they hold unwanted state
Last updated on