Workflows
A multi-agent workflow is several agents run in sequence or in parallel, with output handed from one to the next. There is no orchestration DSL to learn. The composition lives in your code, where you can debug it.
Sequential workflows#
composeAgents chains agents, passing each result to the next.
import { CoreSource, composeAgents, createAgent } from '@coresourceai/sdk';
const coresource = new CoreSource();
const researcher = await coresource.agents.retrieve('agt_researcher');
const pipeline = composeAgents(researcher, {
agent: createAgent({
type: 'agent',
systemPrompt: 'Turn the upstream evidence into an executive brief.',
outputs: ['brief.md']
})
});
const { result, steps } = await pipeline.run('Research invoice automation.');
console.log(`${steps.length} stages`);
console.log(result.text);composeAgents accepts both saved-agent handles and createAgent() handles, so a pipeline can mix a shared server-side definition with a local agent that uses your tools.
Controlling the handoff#
By default each stage receives the previous stage's text, structured output, and artifacts. handoff changes how artifacts travel; structured output is passed as input.upstream in every mode when it exists.
handoff | Behaviour |
|---|---|
'artifacts' (default) | Pass text and structured output, plus prior artifacts as AgentInput references |
'files' | Pass text and structured output; download artifacts and re-upload them as files |
'none' | Pass text and structured output without artifacts |
On the native hosted path, 'artifacts' keeps the handoff server-side. If the downstream createAgent() uses custom tools, runtime tools, a local skill bundle, or local MCP, that agent's SDK path downloads referenced artifacts and uploads them to its SDK-hosted runtime. Choose 'files' when you explicitly want the composition layer to perform that conversion for every downstream agent.
For full control, supply a prompt function:
import { composeAgents, createAgent } from '@coresourceai/sdk';
const researcher = createAgent({ type: 'saga', outputs: ['research.md'] });
const writer = createAgent({ type: 'agent', outputs: ['brief.md'] });
const pipeline = composeAgents(researcher, {
agent: writer,
prompt: ({ previous, step, results }) => ({
prompt: `Write a one-page brief from this research:\n\n${previous.text}`,
input: { stage: step, upstreamRuns: results.map((r) => r.id) },
inputs: previous.artifacts.map((artifact) => ({
type: 'artifact',
runId: previous.id,
artifactId: artifact.id,
path: `upstream/${artifact.name ?? artifact.id}`
}))
})
});
const { result } = await pipeline.run('Research invoice automation.');The context gives you initial, previous, results, and step. Enough to build a prompt from anywhere in the chain, not just the stage before.
Parallel workflows#
Parallelism is plain Promise.all. There is no special construct because none is needed.
import { CoreSource } from '@coresourceai/sdk';
const coresource = new CoreSource();
const [eu, us, apac] = await Promise.all([
coresource.agents.run('agt_researcher', 'Research EU invoice benchmarks.'),
coresource.agents.run('agt_researcher', 'Research US invoice benchmarks.'),
coresource.agents.run('agt_researcher', 'Research APAC invoice benchmarks.')
]);
console.log([eu, us, apac].map((result) => result.status));One saved definition, three concurrent runs. Fan out with map, and use allSettled when partial results are still useful:
import { CoreSource, type AgentResult } from '@coresourceai/sdk';
const coresource = new CoreSource();
const regions = ['EU', 'US', 'APAC', 'LATAM'];
const settled = await Promise.allSettled(
regions.map((region) =>
coresource.agents.run('agt_researcher', `Research ${region} invoice benchmarks.`)
)
);
const succeeded: AgentResult[] = [];
for (const [index, outcome] of settled.entries()) {
if (outcome.status === 'fulfilled') {
succeeded.push(outcome.value);
} else {
console.error(`${regions[index]} failed:`, outcome.reason);
}
}Fan-out then fan-in#
The common workflow shape: several agents work in parallel, then one synthesises.
import { CoreSource } from '@coresourceai/sdk';
const coresource = new CoreSource();
const regions = ['EU', 'US', 'APAC'];
// Fan out.
const research = await Promise.all(
regions.map((region) =>
coresource.agents.run('agt_researcher', `Research ${region} invoice benchmarks.`)
)
);
// Fan in: both sides are saved agents, so upstream artifacts are passed to
// the hosted synthesiser by reference.
const synthesis = await coresource.agents.run('agt_synthesiser', {
prompt: 'Compare the regional findings and produce one global summary.',
input: { regions },
inputs: research.flatMap((result, index) =>
result.artifacts.map((artifact) => ({
type: 'artifact' as const,
runId: result.id,
artifactId: artifact.id,
path: `regions/${regions[index]}/${artifact.name ?? artifact.id}`
}))
)
});
console.log(synthesis.text);The path prefixes matter. Giving each upstream its own directory means the synthesiser can tell the sources apart instead of finding three files called report.md.
Supervisor and worker#
To let one agent decide when to invoke another, wrap the delegate in a custom tool. The supervisor calls a tool; the tool runs another agent.
import { CoreSource, createAgent } from '@coresourceai/sdk';
const coresource = new CoreSource();
const supervisor = createAgent({
type: 'agent',
systemPrompt:
'You answer questions about company data. Delegate deep research to the ' +
'research tool rather than guessing; synthesise what it returns.',
tools: [
{
name: 'deep_research',
description:
'Run a thorough multi-source research task. Slow (minutes) but ' +
'authoritative. Use for questions needing primary sources.',
inputSchema: {
type: 'object',
properties: {
question: { type: 'string', description: 'One self-contained question.' }
},
required: ['question'],
additionalProperties: false
},
annotations: { title: 'Deep research', readOnlyHint: true },
handler: async (input, { signal, deadlineAt }) => {
if (deadlineAt - Date.now() < 120_000) {
return {
error: 'insufficient_time',
message: 'Research needs ~2 minutes. Answer from what you have.'
};
}
const result = await coresource.agents.run(
'agt_researcher',
String(input.question),
{ signal }
);
return { status: result.status, findings: result.text };
}
}
]
});
const answer = await supervisor.run('What drove the Q2 margin change in DACH?');
console.log(answer.text);
await supervisor.close();This is agent-to-agent delegation built from the tool primitive. The supervisor decides when to delegate; your handler controls the nested prompt, cancellation signal, deadline check, and returned value.
Two things worth doing here that this example shows: check the deadline before starting a long delegate, and return status alongside the text so the supervisor knows whether to trust it.
Choosing a shape#
| Shape | Use when | Built with |
|---|---|---|
| One saga | The work decomposes but is one coherent job | type: 'saga', parallelism |
| Sequential | Distinct stages, each refining the last | composeAgents |
| Parallel | Independent work over the same definition | Promise.all |
| Fan-out/fan-in | Parallel work needing synthesis | Promise.all + inputs |
| Supervisor/worker | The agent decides when to delegate | Custom tool wrapping a run |
Use a single saga when one definition can own the complete plan. Reach for multiple agents when stages need different configurations, models, capabilities, ownership, or trust boundaries.
