FabricFabricHarness
Building Agents

Agent Anatomy

The metadata-first shape of a Fabric Harness agent — default and strict variants of the same call.

Fabric Harness agents are metadata-first. Every agent module must default-export an agent definition created via agent({...}). The call shape is identical from both SDK entrypoints — what differs is whether headless defaults are injected at runtime.

  • Defaultimport { agent } from '@fabric-harness/sdk'. Injects headless defaults (runtime: 'stateless', sandbox: 'virtual', loopRuntime: pi-agent-core, compaction: { enabled: true }) on every init() call. The fast path for prototypes, webhooks, edge agents.
  • Strictimport { agent } from '@fabric-harness/sdk/strict'. Same call shape, no defaults injected. Required for Temporal-backed durability (replay determinism) and recommended for compliance/audit workloads.

Both produce the same AgentDefinition and run identically through fh run, fh build, fh describe, and any deploy target.

Agent definition

import { agent, schema } from '@fabric-harness/sdk';

export default agent({
  name: 'ask',
  input: schema.object({ question: schema.string() }),
  output: schema.string(),
  triggers: { webhook: true },
  run: async ({ init, input }) => {
    const session = await (await init()).session();
    return session.prompt(input.question);
  },
});

init() defaults are injected automatically. Override any of them by passing a value: init({ runtime: 'inline', sandbox: 'docker' }).

import { agent, schema } from '@fabric-harness/sdk/strict';

export default agent({
  name: 'ask',
  input: schema.object({ question: schema.string() }),
  output: schema.string(),
  triggers: { webhook: true },
  run: async ({ init, input }) => {
    const fabric = await init({
      runtime: 'temporal',
      sandbox: 'local',
      compaction: { enabled: false },
    });
    const session = await fabric.session();
    return session.prompt(input.question);
  },
});

Every option is declared in source. Nothing implicit. Required for Temporal — auto-compaction would break replay determinism.

The function name (agent) and call shape are identical across both imports. Plain default-exported functions are intentionally rejected — requiring an agent({...}) call keeps agents discoverable and gives the CLI typed metadata.

The FabricContext

When the CLI invokes an agent it provides:

interface FabricContext<TInput = JsonObject> {
  payload: TInput;
  input: TInput; // validated input for metadata agents
  init(options?: AgentInit): Promise<FabricAgent>;
}

Use input for typed, schema-validated values inside run:

run: async ({ init, input }) => {
  const fabric = await init();
  const session = await fabric.session();
  return session.prompt(input.question);
}

init() options

Agents from either entrypoint call the same init() to construct the runtime:

const fabricAgent = await init({
  id: 'agent-1',
  model: 'openai/gpt-5.5',
  role: 'engineer',                 // Markdown role file under .fabricharness/roles/
  sandbox: 'local',                 // 'virtual' | 'empty' | 'local' | 'docker' | 'cloudflare' | factory
  autonomy: {
    mode: 'background',
    onMissingInput: 'assume',
    onApprovalUnavailable: 'fail',
    onCredentialMissing: 'fail',
  },
});

When using the bare @fabric-harness/sdk import you typically omit runtime, sandbox, and loopRuntime — those defaults are injected. Override anything you like; defaults fill the gaps you don't set.

⚠️ Temporal users: if you set runtime: 'temporal' from the bare import, the SDK emits a one-time console.warn. Auto-compaction is non-deterministic across Temporal replay. Either switch to @fabric-harness/sdk/strict or pass compaction: { enabled: false } explicitly.

Triggers

Declare triggers inside agent({...}). The Node and Cloudflare server targets respect:

export default agent({
  name: 'triage',
  triggers: {
    webhook: true,    // POST /agents/:agent/:id
    // schedule: '*/15 * * * *',  // future
    // cli: true,                  // CLI-only, default true
  },
  async run(ctx) {
    return ctx.input;
  },
});

Where to go next