FabricFabricHarness
Deployment

Build and Run Artifacts

Package v2 jobs and persistent agents into self-contained deployable artifacts.

fh build compiles the embedded workspace, bundles authored definitions, and emits a target-specific runtime. Node-derived targets use the same v2 HTTP server as fh dev, so local and deployed routes have the same semantics.

What the build binds

fh build does more than copy source. The generated manifest describes definitions, public routes, policy, required capabilities, runtime versions, package coordinates, artifact digest, and provenance so another system can verify the artifact before it runs.

RevenueOpsAgent source becoming a verified build manifest and portable Node, Docker, Temporal, Cloudflare, and Databricks targets
Representative UIThe build manifest binds definitions, policy, runtime requirements, capabilities, package versions, digest, and provenance.

Start with both definition types

Finite work lives in .fabricharness/jobs/:

.fabricharness/jobs/summarize.ts
import { defineAgent, schema } from '@fabric-harness/sdk';

export default defineAgent({
  name: 'summarize',
  input: schema.object({ text: schema.string() }),
  output: schema.string(),
  triggers: { webhook: true },
  async run({ init, input }) {
    const session = await (await init()).session();
    return session.prompt(`Summarize:\n\n${input.text}`);
  },
});

Long-lived addressable agents live in .fabricharness/agents/:

.fabricharness/agents/support.ts
import { createAgent } from '@fabric-harness/sdk';

export default createAgent(({ id }) => ({
  name: 'support',
  instructions: `You are the support agent for account ${id}.`,
  model: 'openai/gpt-5.5',
  triggers: { webhook: true },
}));

Build Node

fh build --target node

The build summary distinguishes the two surfaces:

Build complete
  Output    .fabricharness/build/node
  Manifest  .fabricharness/build/node/manifest.json
  Jobs      summarize
  Agents    support

The artifact contains the shared server and self-contained definition modules:

.fabricharness/build/node/
  .fabricharness/
    agents/support.mjs
    jobs/summarize.mjs
    roles/
    skills/
  dist/
    agents/support.mjs
    jobs/summarize.mjs
    server.mjs
  manifest.json
  package.json

Monorepo contracts stay inside the artifact

Definitions can import a shared contracts package owned by the same pnpm workspace:

agents/package.json
{
  "dependencies": {
    "@acme/agent-contracts": "workspace:*",
    "zod": "catalog:"
  }
}

Harness follows and bundles dependencies resolved through workspace:, catalog:, file:, link:, portal:, and patch: protocols. Those source-only specifiers are omitted from the artifact package.json. The resulting directory can be copied to a clean machine, staged in a Unity Catalog Volume, or handed to a delivery system such as Fabric Runway without the original monorepo or a sibling checkout.

For the complete producer/consumer contract, detached verification script, runtime-binding boundary, and failure behavior, see Portable agent packages.

Registry dependencies intentionally externalized by the server remain in the generated package.json; install those in the artifact directory before starting it. A detached artifact must never contain a workspace: or catalog: dependency.

Run it:

PORT=8080 OPENAI_API_KEY="$OPENAI_API_KEY" \
  node .fabricharness/build/node/dist/server.mjs

Invoke the built artifact

Finite jobs return their result and a generated run ID:

curl -sS http://localhost:8080/jobs/summarize \
  -H 'content-type: application/json' \
  -d '{"text":"Fabric builds durable agents."}'
{
  "result": "Fabric is a framework for durable agents.",
  "runId": "8f2d...",
  "agentPath": "/app/.fabricharness/jobs/summarize.mjs"
}

Persistent input is admitted durably and returns 202:

curl -i http://localhost:8080/agents/support/acct-42 \
  -H 'content-type: application/json' \
  -d '{"message":"Where is my invoice?"}'
{
  "submissionId": "5ec1...",
  "streamUrl": "/agents/support/acct-42/conversation?session=default&offset=0",
  "offset": "0"
}

Use @fabric-harness/client to wait for settlement:

import { createFabricClient } from '@fabric-harness/client';

const client = createFabricClient({ baseUrl: 'http://localhost:8080' });
const admitted = await client.agents.send({
  agent: 'support',
  id: 'acct-42',
  message: 'Where is my invoice?',
});
const settled = await client.agents.wait({
  agent: 'support',
  id: 'acct-42',
  submissionId: admitted.submissionId,
});

Manifest schema v2

Every current artifact emits schemaVersion: 2 with separate collections:

jq '{schemaVersion, jobs, agents}' .fabricharness/build/node/manifest.json

Build readers in @fabric-harness/node normalize legacy schema-v1 manifests, but external tooling should migrate to jobs and agents. See Build manifest.

Production exposure

In production, a public job or persistent agent must declare triggers.webhook: true. Protect the server and scope tenants with environment variables:

NODE_ENV=production \
FABRIC_HARNESS_API_TOKEN="$API_TOKEN" \
FABRIC_HARNESS_TENANT_REQUIRED=1 \
FABRIC_HARNESS_RATE_LIMIT_MAX=100 \
FABRIC_HARNESS_RATE_LIMIT_WINDOW_MS=60000 \
node dist/server.mjs

Clients then send both headers:

curl http://localhost:8080/jobs/summarize \
  -H "authorization: Bearer $API_TOKEN" \
  -H 'x-fabric-tenant: acme' \
  -H 'content-type: application/json' \
  -d '{"text":"..."}'

Other targets

fh build --target docker --docker-build --docker-tag acme/support:1.0.0
fh build --target databricks-app
fh build --target aks
fh build --target aca

Node-derived targets inherit the shared v2 server. Cloudflare supports finite jobs and serialized Durable Object-backed persistent sessions. The Cloudflare runtime intentionally omits the Node submission queue, abort, attachment materialization, and offset conversation-stream routes.

Verify supply-chain metadata

fh build --target docker --provenance --attestation --sbom
fh verify-provenance .fabricharness/build/docker
fh verify-attestation .fabricharness/build/docker

See Docker, Databricks App, and Security hardening.