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.

Start with both definition types
Finite work lives in .fabricharness/jobs/:
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/:
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 nodeThe build summary distinguishes the two surfaces:
Build complete
Output .fabricharness/build/node
Manifest .fabricharness/build/node/manifest.json
Jobs summarize
Agents supportThe 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.jsonMonorepo contracts stay inside the artifact
Definitions can import a shared contracts package owned by the same pnpm workspace:
{
"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.mjsInvoke 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.jsonBuild 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.mjsClients 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 acaNode-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/dockerSee Docker, Databricks App, and Security hardening.