Composition examples
Composition means connecting small tasks through their inputs and outputs. You use ordinary TypeScript to decide what runs next; Stratos supplies the validation, configuration, and resource lifecycle around each primitive.
Start with the child-workflow example below. It extends the greeting workflow from Author a workflow without requiring credentials. The agent and Decision examples are separate workflows and require provider setup before execution.
Child workflows
Keep the existing greet workflow and create a sibling directory,
.stratos/workflows/greet-team/. Create its workflow.yaml:
name: greet-team
shortDescription: Greet a list of people in uppercase by reusing the greet workflow.
longDescription: |
Requires the greet workflow in the same discovery root. Supply names to
receive uppercase messages in input order. An empty list returns no messages.
No credentials or external calls are needed.
version: "1"
Create .stratos/workflows/greet-team/workflow.ts:
import {workflow} from '@stratoscli/stratos';
import {z} from 'zod';
import createGreet from '../.generated/greet.js';
export default workflow({
input: z.strictObject({
names: z.array(z.string().min(1)).describe('People to greet'),
}),
output: z.strictObject({
messages: z.array(z.string()).describe('Greetings in input order'),
}),
async run(input) {
const greet = createGreet();
const results = await Promise.all(
input.names.map(name => greet.execute({name, loud: true})),
);
return {messages: results.map(result => result.message)};
},
});
From the workspace directory, build and run the parent:
stratos build greet-team
stratos list --json
stratos describe greet-team --json
stratos run greet-team --input '{"names":["Ada","Grace"]}' --json
The result’s data is:
{"messages":["HELLO, ADA!","HELLO, GRACE!"]}
The import refers to a generated wrapper, not greet/workflow.ts. Building
the parent builds the required child and generates the wrapper before checking
the parent’s types. Both workflows must be in the same discovery root; do not
write the .generated/ files yourself.
createGreet() binds the child reference without running it. Each execute
starts a fresh child invocation and returns its output directly, without the
CLI envelope. Promise.all allows independent calls to run concurrently and
preserves the input order in its result array. An empty names array returns
an empty messages array without starting a child.
The child uses its own configuration, not its parent’s agent settings. You can bind named child-agent or Decision overrides when creating the object; see the wrapper contract. Repeated and parallel calls are supported, but invoking an active ancestor workflow is an error.
Agents
Use an agent when a step needs language reasoning or a conversation. This
example accepts text and returns a summary. Create a summarize directory
under the same workflow root and add this workflow.yaml:
name: summarize
shortDescription: Summarize supplied text using an agent; use when the source text is already available.
longDescription: |
Supply text and receive a model-generated summary. Configure native Codex
authentication and an available model before execution; requests may cost
money. The prompt requests no tools, but tool restrictions do not guarantee
read-only execution. Repeated runs make new requests; wording may differ.
version: "1"
agents:
summarizer:
harness: codex
model: [openai/gpt-6-astra]
allowedMcps: []
allowedTools: []
systemPrompt: Summarize the supplied text briefly without using tools.
The model is an example, not a universal default. Configure an available
model/provider and working native Codex authentication, or select another
supported harness before running it. allowedMcps: [] disables workflow MCPs;
allowedTools: [] removes every mapped, removable built-in tool.
This is not a tool-free or read-only guarantee: Codex’s edit and write
remain available through apply_patch, and unmapped tools are unaffected.
The system prompt requests no tool use but does not enforce it. Inspect
stratos tools list codex and read
tool selection and its limits
before executing.
Create workflow.ts:
import {createAgent, workflow} from '@stratoscli/stratos';
import {z} from 'zod';
export default workflow({
input: z.strictObject({
text: z.string().describe('Text to summarize'),
}),
output: z.strictObject({
summary: z.string().describe('Short summary'),
}),
async run(input) {
const summarizer = createAgent({configId: 'summarizer'});
const summary = await summarizer.execute(input.text);
return {summary};
},
});
After configuring access, build and run it:
stratos build summarize
stratos list --json
stratos describe summarize --json
stratos run summarize --input '{"text":"The release adds CSV export. PDF export is still being investigated."}' --json
The output contains a model-generated summary; wording is not deterministic
and the request may incur charges. Creation is synchronous, but the first
execution initializes the harness. A later execute on the same object
continues that conversation. Unlike a child-workflow object, one agent object
cannot accept overlapping calls: use separate agents for parallel work.
This workflow also meets the {text} to {summary} contract used in the
Overview’s composition example.
The parent need not know which harness produced the summary.
For typed agent answers, prompt templates, restrictions, or local models,
use the agent reference.
Decision
Use a structured decision when you need a category or score rather than a
conversation. Create .stratos/workflows/classify-request/ with this manifest:
name: classify-request
shortDescription: Classify a request as billing or technical using a structured model decision.
longDescription: |
Requires JEV_API_KEY. Supply request text and receive a billing or technical
category. Each run sends the text to Jev and may incur charges; only send
authorized data. Model decisions may vary and provider failures propagate.
version: "1"
decision:
classifier:
model: jev-1.13.0
apiKey: "{env:JEV_API_KEY}"
Create workflow.ts:
import {choice, decision, workflow} from '@stratoscli/stratos';
import {z} from 'zod';
export default workflow({
input: z.strictObject({
text: z.string().describe('Request to classify'),
}),
output: z.strictObject({
category: z.enum(['billing', 'technical']).describe('Selected category'),
}),
async run(input) {
const result = await decision({
configId: 'classifier',
state: input.text,
questions: {
category: choice('What kind of request?', {
billing: 'Payments or refunds',
technical: 'Broken functionality',
}),
},
});
return {category: result.answers.category.choice};
},
});
Provide JEV_API_KEY privately through the environment before execution.
Then:
stratos build classify-request
stratos list --json
stratos describe classify-request --json
stratos run classify-request --input '{"text":"I need help with a refund."}' --json
The intended category is billing, but this is a model decision, not a
guaranteed answer. choice constructs the question; decision makes the
request. Code can use the selected category to choose a child workflow or
decide whether to invoke an agent.
You can select a compatible installed Ollama decision model through configuration instead. The Decision reference explains provider setup, other question types, probability semantics, and portability limits. Changing providers does not guarantee equivalent decisions.
Handle results and failures deliberately
All three examples return domain data from run. Agents, children, Decision,
and direct MCP tools throw or reject on failure; they do not return a successful
placeholder result. Catch a failure only where your workflow has a defined
recovery behavior. Otherwise let it propagate so the run reports failure and
cleans up owned resources.
Await work before returning. A handled child error does not fail its parent; an unhandled parent error cancels active descendants. Cleanup does not undo completed side effects, and a rerun is not an automatic resume.
For an end-to-end workflow that combines deterministic code with Decision decisions, read GitHub Actions security review. For direct tool calls without an agent, continue with MCP tools.