Headless agents with the default import
The 10-line path to a working headless agent — defineAgent({...}) with headless defaults injected.
Fabric Harness separates three choices that are easy to mix up:
- Entrypoint —
@fabric-harness/sdkfor headless defaults, or@fabric-harness/sdk/strictfor no implicit behaviour. - Runtime —
stateless,inline, ortemporal. - Target — where the agent runs, such as
node,temporal-worker,docker, orcloudflare.
This page shows the smallest headless setup: the default @fabric-harness/sdk import with headless defaults. The same init(), session.prompt(), session.shell(), session.skill(), and session.task() APIs work from @fabric-harness/sdk/strict too.
One SDK, two import entrypoints. The minimal entrypoint is not a separate framework and not a less capable runtime. It keeps imports small and applies headless defaults. Durability still comes from
runtime, not the import path.
The 7-line agent
import { defineAgent } from '@fabric-harness/sdk';
export default defineAgent<{ message: string }>({
name: 'echo', triggers: { webhook: true },
run: async ({ init, input }) => {
const session = await (await init({ model: 'openai/gpt-5.5' })).session();
return { reply: await session.prompt<string>(input.message) };
},
});Run it:
export FABRIC_MODEL=openai/gpt-5.5
export OPENAI_API_KEY=...
fabric-harness run echo --message "hello"Or expose it as a webhook:
fabric-harness dev
curl -X POST http://localhost:3000/jobs/echo \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'That's it. No schemas to define, no policy to author, no session store to configure.
Defaults applied automatically
The default @fabric-harness/sdk import injects headless defaults so the snippet stays minimal. Each is overridable per call:
| Default | Why | Override with |
|---|---|---|
runtime: 'stateless' | Headless / edge / one-shot agents often do not need persistence | init({ runtime: 'inline' }) to keep an in-memory session, or 'temporal' for durable workflows |
sandbox: 'virtual' | In-memory bash + filesystem (grep, glob, read, cat, mkdir, rm, echo) via just-bash. No host shell access. | init({ sandbox: 'local' }) for host shell, 'docker' for isolation, or a SandboxFactory |
loopRuntime: pi-agent-core | Multi-provider model coverage from @earendil-works/pi-ai — Anthropic, OpenAI, Google, OpenAI-compatible proxies, OpenRouter, etc. | init({ loopRuntime: defaultLoopRuntime }) to use Fabric's NativeLoopRuntime (with capability-policy + result-retry hooks) |
compaction: { enabled: true } | Long headless sessions compact automatically before they exceed model context limits. | Pass an explicit compaction option to init() or use /strict for fully explicit runtime configuration. |
For Temporal-backed agents or workloads that require every runtime option to be explicit, import
defineAgent from @fabric-harness/sdk/strict. Typed I/O, policy, approvals, artifacts, and metadata are
available from both entry points.
What the pieces do
defineAgent({...})
defineAgent<TInput>({ run }) gives the handler a compile-time input type. Without input and output
schemas, values pass through without runtime validation. Add schemas when inputs cross a trust
boundary or other code depends on a stable result shape.
runtime: 'stateless'
Disables session persistence. No store writes, no artifact disk persistence, no approval waiting. Each invocation is independent. This is the right choice for:
- High-volume webhook handlers where state would just be discarded.
- Edge runtimes (Cloudflare Workers, Vercel Edge) with no durable storage attached.
- Quick prototypes where conversation continuity isn't yet a concern.
In production (FABRIC_ENV=production or NODE_ENV=production), choosing inline without an explicit SessionStore will emit a warning unless you set FABRIC_ALLOW_EPHEMERAL_STATE=1 or pick runtime: 'stateless' explicitly. This prevents surprise data loss.
Default and strict entry points
The default entry point exports the complete public SDK and applies friendly headless defaults.
@fabric-harness/sdk/strict exports the same core primitives but does not inject runtime, sandbox,
loop-runtime, or compaction defaults.
Minimal does not mean feature-light
The minimal entrypoint skips import weight and ceremony, not the shared agent primitives. Skills, roles, scoped commands, MCP tools, and tasks all work — they just do not require typed input/output metadata or a policy authoring step.
Skills (Markdown-defined procedures)
Drop a Markdown file under .fabricharness/skills/ and call it from your agent. Skills are discovered automatically by the workspace loader.
---
name: triager
description: Search the knowledge base for the best matching article and write a concise, friendly reply.
---
You are a customer support triager.
Steps:
1. Use grep / glob / read over `/workspace/kb` to find articles relevant to the customer's question.
2. Quote the most directly relevant lines.
3. Write a 2–4 sentence reply in the brand's voice (friendly, concise, no jargon).
4. If nothing matches, say so honestly and suggest the customer contact support@example.com.import { defineAgent } from '@fabric-harness/sdk';
export default defineAgent<{ message?: string }>({
name: 'support', triggers: { webhook: true },
run: async ({ init, input }) => {
const session = await (await init({ runtime: 'stateless' })).session();
const message = input.message ?? '';
const reply = await session.skill<string>('triager', { args: { message } });
return { reply };
},
});session.skill('triager', { args }) injects the skill body as the prompt, templates the args in, and returns the model's response. The same primitive exists from both entrypoints. Complete metadata agents can also declare default inline skills with defineAgent({ skills: [...] }) and can add result: schema for typed validation.
Roles (system-prompt overlays)
Roles let you change the agent's voice or expertise without rewriting the prompt every time. Drop a Markdown file under .fabricharness/roles/:
---
name: concise
description: Reply in 1-3 sentences. No bullet points unless explicitly asked. Plain prose only.
---
You write extremely concise replies. Aim for 1–3 sentences. Use plain prose,
not bullet points or headings, unless the user asked for structured output.
Never repeat the question back. Never apologize for the length of the answer.Use it on a session, prompt, or skill call:
const session = await agent.session('thread-1', { role: 'concise' });
await session.prompt('What does this code do?'); // uses 'concise'
await session.skill('triager', { args, role: 'concise' }); // override on the callPrecedence: per-call role > session role > agent role.
Scoped commands (privileged CLIs)
defineCommand binds a CLI like gh or npm with its env (and therefore its secrets) at the command level — secrets never enter model context. Grant the command per-call:
import { defineAgent, defineCommand } from '@fabric-harness/sdk';
const gh = defineCommand('gh', { env: { GH_TOKEN: process.env.GH_TOKEN } });
const npm = defineCommand('npm');
export default defineAgent<{ issueNumber?: number }>({
name: 'triage',
run: async ({ init, input }) => {
const session = await (await init({ sandbox: 'local', runtime: 'stateless' })).session();
const result = await session.skill<string>('triage-issue', {
args: { issueNumber: input.issueNumber },
commands: [gh, npm], // available only inside this skill call
});
return { result };
},
});The agent can run gh issue view 42 ... because gh is in commands. It cannot read GH_TOKEN directly — the token is bound to the gh invocation by the runtime, not exposed to the model.
Tasks (delegated subagents)
session.task(prompt, options?) runs a child session with its own message history but the same sandbox/filesystem — perfect for parallel research or focused subgoals.
const research = await session.task('Read /workspace/kb/billing/* and summarize the refund policy in 5 bullets.');
const reply = await session.prompt(`Use this research to answer the customer:\n\n${research}`);Tasks support a role and cwd override, so you can run them as a "researcher" without changing the parent role.
Mounting a knowledge base
Combine withFilesystemSources with the bare @fabric-harness/sdk import for a compact support agent in ~25 lines:
import { defineAgent, localDirectorySource, withFilesystemSources } from '@fabric-harness/sdk';
const sandbox = withFilesystemSources('empty', [{
mountAt: '/workspace/kb',
source: localDirectorySource('./knowledge-base', { include: (p) => p.endsWith('.md') }),
}]);
export default defineAgent<{ message?: string }>({
name: 'support', triggers: { webhook: true },
run: async ({ init, input }) => {
const session = await (await init({ sandbox, runtime: 'stateless' })).session();
const reply = await session.skill<string>('triager', {
args: { message: input.message ?? '' },
});
return { reply };
},
});See Filesystem sources for more.
MCP tools
connectMcpServer works the same from the bare @fabric-harness/sdk import — just don't forget to close() the connection in a finally:
import { connectMcpServer, defineAgent } from '@fabric-harness/sdk';
export default defineAgent<{ prompt?: string }>({
name: 'gh-assist',
run: async ({ init, input, env }) => {
const github = await connectMcpServer('github', {
url: 'https://mcp.github.com/mcp',
headers: { Authorization: `Bearer ${env?.GITHUB_TOKEN}` },
});
try {
const session = await (await init({ tools: github.tools, runtime: 'stateless' })).session();
return await session.prompt(input.prompt ?? '');
} finally {
await github.close();
}
},
});Progressive disclosure: when to switch to /strict
The minimal entrypoint can be a starting point. Add production features one by one as the work demands them:
| You want to... | Switch to... | What changes in the agent |
|---|---|---|
| Validate inputs / outputs | defineAgent({ input, output, run }) | Add Fabric schemas; definitions without schemas pass values through. |
| Restrict shell / tools | policy: CapabilityPolicy on init or session.prompt | Allow/deny/requireApproval lists. |
| Bind a privileged CLI | defineCommand('gh', { env: { GH_TOKEN } }) + commands: [gh] | Secrets bound at the command, never exposed to the model. |
| Run a Markdown skill | session.skill('triage', { args, result }) | Drop the skill into .fabricharness/skills/triage.md. |
| Persist conversations | runtime: 'inline' + a SessionStore (SQLite/Postgres/file from @fabric-harness/node) | Same init(), just pass store. |
| Write durable artifacts | session.artifact(name, content) | Requires a store that implements putArtifact. |
| Survive crashes / restarts | runtime: 'temporal' + @fabric-harness/temporal | The agent code is unchanged; the runtime swaps. |
Minimal vs complete entrypoint at a glance
import { defineAgent } from '@fabric-harness/sdk';
export default defineAgent({
name: 'echo',
run: async ({ init, input }) => {
const session = await (await init({ runtime: 'stateless' })).session();
return { reply: await session.prompt<string>(String(input.message ?? '')) };
},
});import { defineAgent, schema } from '@fabric-harness/sdk/strict';
import type { CapabilityPolicy } from '@fabric-harness/sdk/strict';
const policy: CapabilityPolicy = {
commandPolicy: {
allow: ['git status*', 'git diff*'],
deny: ['git push*'],
requireApproval: ['gh issue comment**'],
},
maxCommandTimeoutMs: 30_000,
};
const inputSchema = schema.object({ issueNumber: schema.number(), title: schema.string() });
const outputSchema = schema.object({ severity: schema.enum(['low','medium','high']), summary: schema.string() });
export default defineAgent({
name: 'triage',
input: inputSchema,
output: outputSchema,
model: process.env.FABRIC_MODEL,
skills: [{ name: 'triage', content: 'Triage the issue and return a concise structured result.' }],
run: async ({ init, input }) => {
const session = await (await init({ policy })).session();
const result = await session.skill('triage', { args: input, policy, result: outputSchema });
await session.artifact('triage.md', `# ${input.title}`, { contentType: 'text/markdown' });
return result;
},
});The CLI, dev server, and deploy targets are shared. The SDK entrypoint and runtime choice are independent; see SDK entrypoints, runtimes, and targets.
Anti-patterns
- Don't fork your code into minimal and complete copies. Graduate features in place when you need typed I/O, policy, durable stores, telemetry, or provider classes.
- Don't use
runtime: 'stateless'when you need approvals. Stateless mode has no durable store, so approval waiting and later resolution are unavailable. - Don't import from both
@fabric-harness/sdkand@fabric-harness/sdk/strictin the same job. Pick the default-injecting or explicit entry point for that module. - Don't assume durability from the import path. Use
runtime: 'temporal'or a durable store explicitly.
See also
- examples/minimal — the 10-line example end-to-end.
- examples/support-agent — minimal entrypoint + filesystem-mounted knowledge base.
- SDK entrypoints, runtimes, and targets — how the choices fit together.
- Runtime modes —
inlinevsstatelessvstemporal. - Filesystem sources — mount read-only content into the sandbox.