Composable multi-agent pipelines on top of the Claude Code CLI.
npx @generata/core init @generata/starter ~/Projects/hello-generata
# or: pnpm dlx @generata/core init @generata/starter ~/Projects/hello-generataimport {
defineAgent,
defineWorkflow,
defineConfig,
runWorkflow,
runAgent,
runScript,
worktree,
} from "@generata/core";defineAgent / defineWorkflow / defineConfig author agents, workflows, and a project config. runWorkflow / runAgent drive them from your own TypeScript without going through the CLI, and runScript wraps a driver script with SIGINT/exit-code handling. worktree(...) declares git-worktree isolation:
defineWorkflow({
isolation: worktree({ sharedPaths: ["state.md"] }),
// ...
});Optional boolean. full-permission agents (worker, planner) implicitly receive Read, Glob, and Grep as baseline tools so they can inspect the workspace before writing. Set filesystemAccess: false to omit that baseline - useful for write-or-exec-only agents that should not be allowed to read arbitrary files. Omitting the field keeps the baseline (the default).
The flag is a no-op on read-only agents (filesystem read is the defining characteristic of that permission level); setting it to false on a read-only agent is a parse error.
import { defineAgent } from "@generata/core";
export default defineAgent({
name: "shell-runner",
type: "worker",
permissions: "full",
filesystemAccess: false,
modelTier: "light",
description: "Runs a single shell command without reading source.",
tools: ["bash"],
prompt: ({ command }) => `Run: ${command}`,
});@generata/core exposes runWorkflow and runAgent so you can drive any workflow or agent from your own TypeScript without going through the CLI. This is the primitive for loops, batch jobs, or wrapping generata in a larger script.
import { runWorkflow } from "@generata/core";
import reviewNote from "./workflows/review-note.js";
const result = await runWorkflow(reviewNote, { file: "notes/aardvark.md" });
console.log(result.output);
if (!result.success) process.exit(1);import { glob } from "node:fs/promises";
import { runWorkflow } from "@generata/core";
import reviewNote from "./workflows/review-note.js";
for await (const file of glob("notes/*.md")) {
const result = await runWorkflow(reviewNote, { file });
if (!result.success) {
console.error(`Failed on ${file}:`, result.steps.at(-1)?.output);
continue;
}
console.log(`Reviewed ${file}: ${result.output}`);
}When a workflow declares isolation: worktree(...), every runWorkflow call by default sets up and tears down its own worktree. If you want one shared worktree across an iteration, set it up yourself, pass isolation: "none" to disable per-run setup, and point cwd at the worktree path:
import { runWorkflow } from "@generata/core";
import processItem from "./workflows/process-item.js";
// Replace with whatever produces a worktree path - your own helper, an
// existing checkout, etc. The point is: one path, many iterations.
const worktreePath = await setupSharedWorktree();
for (const item of items) {
await runWorkflow(processItem, { id: item.id }, { isolation: "none", cwd: worktreePath });
}A programmatic run prints two lines to stderr at start so you know what's running and where the log lives:
workflow: notes/review-note (3 steps)
Full log: file:///abs/path/to/.generata/logs/workflow/review-note-<runId>.log
(For runAgent the first line is agent: <name> [<type>].) The Full log: line fires whenever a prompt log is being written - both CLI and programmatic, with or without onEvent. The header line is suppressed when you wire onEvent, since you're driving display yourself (the CLI does this via consoleSink's richer workflow-start / agent-welcome lines).
Prompt logs land at <logsDir>/<kind>/<caller>-<name>-<runId>.log (controlled by logPrompts: true in your config, the default), where <caller> is the basename of the script that called runWorkflow / runAgent. The prefix makes it easy to tell apart logs from different scripts that drive the same workflow. CLI runs and explicit promptLogFile overrides skip the prefix.
Programmatic runs are otherwise silent on the console. Pass onEvent to receive structured events (workflow-start, step-start, step-done, etc.):
await runWorkflow(
processItem,
{ id: "42" },
{
onEvent: (e) => {
if (e.type === "step-done") process.stderr.write(".");
},
},
);runScript wraps a driver script's main function with the canonical lifecycle contract, so every script doesn't re-implement it:
import { runScript, runWorkflow } from "@generata/core";
import longRun from "./workflows/long-run.js";
await runScript(async ({ signal }) => {
await runWorkflow(longRun, { input }, { signal });
});What it does:
- First
^Cprints^C - aborting...and aborts thesignalyou thread intorunWorkflow/runAgentcalls; a second^Cfalls through to Node's default hard-kill. - An
AbortErrorescaping your fn prints<script> cancelledand exits130. - Any other error prints
<script> failed: <message>and exits1- so inside the fn you justthrowinstead of hand-rollingconsole.error+process.exit. <script>is derived from the calling file's basename (audit.ts->audit), the same convention prompt-log prefixes use.- Exit codes are set via
process.exitCode, notprocess.exit(), so stdio flushes naturally.
If you need custom wiring (your own signal source, embedding in a larger process), skip runScript and pass any AbortSignal directly:
const ac = new AbortController();
process.once("SIGINT", () => ac.abort());
await runWorkflow(longRun, { input }, { signal: ac.signal });runWorkflow throws for:
GenerataPrecheckError- workflow misconfigured (introspecterr.issuesfor diagnostics).AbortError- caller cancelled viaAbortSignal.- Infra errors (cannot spawn
claude, etc.).
It returns success: false for:
- A step that hits its
maxRetrieslimit. Inspectresult.steps.at(-1).outputandresult.steps.at(-1).metrics.errorfor diagnostics.
It returns halted: true, success: false for:
- A worker that emits
--halt "<reason>"via the emit bin.result.haltReasoncarries the reason; this is a clean structured stop, not a failure.
Looking to expose workflows over HTTP? See @generata/serve.
| Command | Purpose |
|---|---|
generata <name> |
Run a workflow (shorthand for workflow <name>) |
generata init <template> [dest] |
Scaffold a new project from a template |
generata add <template> |
Merge a template into the current project |
generata agent <name> |
Run a single agent |
generata workflow <name> |
Run a workflow (alias: run) |
generata validate [--all] |
Static-check workflow definitions |
generata metrics [today|week] |
Show metrics summary |
generata commands sync |
Regenerate .claude/commands/ from workflows |
generata worktree prune |
Remove orphan generata/wt-* worktrees and branches |
generata help [topic] |
Show help (topics: agents, workflows, env, templates, bins) |
Workflow flags: --worktree forces git-worktree isolation for the run; --local forces it off (mutually exclusive).
Every field accepted by defineConfig (i.e. the GlobalConfig schema):
| Field | Type | Default | Description |
|---|---|---|---|
modelTiers.heavy/standard/light |
string |
(required) | Model IDs for each cost tier |
workDir |
string |
(required) | Root directory for worktree isolation |
agentsDir |
string |
"agents" |
Directory scanned for agent definition files |
metricsDir |
string |
"metrics" |
Directory where per-run metrics JSON files are written |
logsDir |
string |
"logs" |
Directory where prompt logs are written (when logPrompts is true) |
logPrompts |
boolean |
true |
Write full prompt/response logs for every run |
verboseOutput |
boolean |
false |
Stream each agent's raw Claude output to the console |
showPricing |
boolean |
false |
Print token cost breakdown after each run |
showWeeklyMetrics |
boolean |
true |
Print a weekly usage summary on CLI startup |
notifications |
boolean |
true |
Send OS/Telegram notifications on workflow completion |
maxCriticRetries |
number |
3 |
How many times a critic agent may loop before the step fails |
telegram |
{ botToken, chatId } |
- | Telegram bot credentials for completion notifications (requires notifications: true) |
serve |
unknown |
- | Passed through to @generata/serve; core does not validate the shape |
generata init accepts:
@generata/<alias>- resolves via the built-in catalog<owner>/<repo>- resolves tohttps://github.com/<owner>/<repo>.gitgit@.../https://....git- any git URL./path//abs/path- a local directory containinggenerata.template.json
--yes- non-interactive; required env values default to manifest examples--skip-preflight- skip required-bin checks--skip-install- skip the package-manager install step (useful offline)
--force- overwrite conflicting files--dry-run- print what would be written--into <subdir>- merge into a subdirectory rather than the project root
pnpm install # links the workspace
pnpm --filter @generata/core build # rebuild dist/ after engine changes
node --test --import tsx packages/core/src/**/*.test.tsThe exports map uses a development condition that points at src/ so workspace dev runs through the TypeScript source via tsx; published consumers see the compiled dist/ output.
See CHANGELOG.md for release notes.