Skip to Content
Next — You're reading the unreleased preview docs. APIs and pages may change before the next release.Switch to the current docs →
How-To GuidesTypeScriptParallel Workers — Fan-Out / Fan-In (TypeScript)

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

Overview

Golem agents process invocations sequentially. Async I/O can overlap cooperatively within one invocation, but it does not provide CPU-thread parallelism. Distribute work across multiple agent instances for parallel invocation execution. This skill covers two approaches:

  1. Child agents via AgentDef.client.newPhantom(id).client — create 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 { z } from 'zod'; import { defineAgent, method } from '@golemcloud/golem-ts-sdk'; export const Worker = defineAgent({ name: 'Worker', id: { id: z.number() }, methods: { process: method({ input: { data: z.string() }, returns: z.string() }), }, }); export const WorkerImpl = Worker.implement({ init: ({ id }) => ({ id: id.id }), methods: { process({ data }) { return `processed-${data}`; }, }, }); export const Coordinator = defineAgent({ name: 'Coordinator', id: { name: z.string() }, methods: { fanOut: method({ input: { items: z.array(z.string()) }, returns: z.array(z.string()) }), }, }); export const CoordinatorImpl = Coordinator.implement({ init: () => ({}), methods: { async fanOut({ items }) { // Use a fresh phantom identity for each child, even across coordinators. const promises = items.map((item, i) => Worker.client.newPhantom({ id: i }).client.process({ data: item })); // Wait for all children to finish. return await Promise.all(promises); }, }, });

Chunked Fan-Out

When spawning many children, batch them to limit concurrency:

async fanOutChunked({ items }) { const chunks = arrayChunks(items, 5); // Process 5 at a time const results: string[] = []; for (const chunk of chunks) { const promises = chunk.map((item, i) => Worker.client.newPhantom({ id: i }).client.process({ data: item })); 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 .trigger() and collect results via Golem promises. A PromiseId is a nested record that holds bigints (the agent ID, the fingerprint of the agent incarnation that created it, and the oplog index), so ship it across the agent boundary as a bigint-aware JSON string:

import { z } from 'zod'; import { defineAgent, method, createPromise, awaitPromise, completePromise, PromiseId, } from '@golemcloud/golem-ts-sdk'; export function encodePromiseId(id: PromiseId): string { return JSON.stringify(id, (_k, v) => (typeof v === 'bigint' ? { '#bigint': v.toString() } : v)); } export function decodePromiseId(text: string): PromiseId { return JSON.parse(text, (_k, v) => v && typeof v === 'object' && '#bigint' in v ? BigInt(v['#bigint']) : v, ) as PromiseId; } export const RegionWorker = defineAgent({ name: 'RegionWorker', id: { region: z.string() }, methods: { runReport: method({ input: { promiseId: z.string() }, returns: z.void() }), }, }); export const RegionWorkerImpl = RegionWorker.implement({ init: ({ id }) => ({ region: id.region }), methods: { runReport({ promiseId }) { const report = `Report for ${this.region}: OK`; completePromise(decodePromiseId(promiseId), new TextEncoder().encode(report)); }, }, }); // Inside a coordinator method handler: async dispatchAndCollect({ regions }) { // Create one promise per child. const promiseIds = regions.map(() => createPromise()); // Fire-and-forget: trigger each child with its (encoded) promise ID. regions.forEach((region, i) => { RegionWorker.client.newPhantom({ region }).client.runReport.trigger({ promiseId: encodePromiseId(promiseIds[i]) }); }); // Collect all results (the agent suspends until each promise completes). return await Promise.all( promiseIds.map(async (pid) => new TextDecoder().decode(await awaitPromise(pid))), ); }

Error Handling

Use Promise.allSettled to handle partial failures:

async fanOutWithErrors({ items }) { const promises = items.map((item, i) => Worker.client.newPhantom({ id: i }).client.process({ data: 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. Only a durable agent can fork, and the forked agent is durable too. Use Golem promises to synchronize between the original and forked agents.

Basic Fork Pattern

import { z } from 'zod'; import { defineAgent, method, fork, createPromise, awaitPromise, completePromise, } from '@golemcloud/golem-ts-sdk'; export const ForkAgent = defineAgent({ name: 'ForkAgent', id: { name: z.string() }, methods: { parallelCompute: method({ input: {}, returns: z.string() }), }, }); export const ForkAgentImpl = ForkAgent.implement({ init: () => ({}), methods: { async parallelCompute() { 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 }) { 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. return await Promise.all( promiseIds.map(async (pid) => new TextDecoder().decode(await awaitPromise(pid))), ); }

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
Need a caller-selected identity✅ Use client.get(id) with a coordinator/job namespaceGenerated phantom ID; durable and addressable with client.getPhantom(id, phantomId)
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
  • Identity: These examples create fresh durable phantoms with newPhantom(...).client. Retain the returned phantom ID to address one later with getPhantom. If using get(id) instead, namespace IDs by coordinator and job to avoid sharing children unintentionally.
  • Cleanup: Durable children and forks persist after the coordinator finishes. Delete only instances the coordinator owns and no caller still needs; never delete shared workers merely because one job finished.