FabricFabricHarness
Reference

Triggers and Public Route Gating

How finite jobs and persistent agents are exposed once deployed.

Triggers are definition metadata that tell generated servers which entrypoints may be exposed in production. Put triggers inside defineAgent({...}) or return them from createAgent(...).

Declaring triggers

import { defineAgent } from '@fabric-harness/sdk';

export default defineAgent({
  name: 'webhook-triage',
  triggers: {
    webhook: true,
  },
  async run({ input }) {
    return input;
  },
});

Default behavior

  • Finite job: webhook: true enables POST /jobs/:name in production.
  • Persistent agent: webhook: true enables POST /agents/:name/:id in production.
  • Without it, the definition remains available to local/dev and CLI flows, but production public invocation returns 403 Forbidden.

Public route gating

For Node-derived deployments, gate public routes with:

  • --auth-token-env <var> (CLI / env) requiring a bearer token.
  • --max-body-bytes to cap body size.
  • --rate-limit-window-ms + --rate-limit-max for per-IP rate limits.

For Cloudflare, apply route-level rules around /jobs/<name> and /agents/<name>/<instanceId> in addition to the Worker bearer token.

Schedule triggers

The Node server runs finite schedule triggers with cron-parser:

export default defineAgent({
  name: 'daily-report',
  triggers: { schedule: '0 9 * * 1-5' },
  async run({ input }) { /* ... */ },
});

Persistent-agent configs reject triggers.schedule: a cron expression alone cannot identify the instance, named session, or message to deliver. Put the schedule on a bounded finite job and dispatch explicitly:

import { defineAgent, dispatch } from '@fabric-harness/sdk';

export default defineAgent({
  name: 'daily-assistant-wakeup',
  triggers: { schedule: '0 9 * * 1-5' },
  async run() {
    return dispatch({
      agent: 'assistant',
      id: 'daily-ops',
      message: 'Review the morning operations queue.',
      dispatchId: `daily-assistant:${input.scheduledAt}`,
      tenantId: 'system:daily-operations',
      actor: { agentId: { id: 'daily-scheduler', type: 'service' } },
    });
  },
});

This keeps the persistent identity and input explicit while the finite wrapper retains the normal bounded scheduling, lease, idempotency, and run-inspection behavior.

Configure timezone and payload resolution when embedding the server:

await startDevServer({
  scheduler: {
    timezone: 'America/Phoenix',
    payload: (jobName, expression, scheduledAt) => ({ scheduledAt: scheduledAt.toISOString() }),
    catchUp: 'run-once',
  },
});

Set FABRIC_HARNESS_SCHEDULER_ENABLED=0 to disable it. The scheduler calculates each next cron boundary before execution, skips overlapping runs of the same job, emits normal session events, and stops its timers during graceful shutdown.

catchUp: "skip" is the default. With a lease store that supports durable cursors, catchUp: "run-once" executes only the most recent occurrence missed during downtime. A first start establishes the cursor and does not replay historical cron time. Public dispatch() accepts dispatchId, tenantId, and actor, so scheduled persistent-agent delivery can be both idempotent and explicitly owned.

Multiple Node replicas

Use the Postgres lease store so every cron occurrence and active job has one owner across replicas:

import { Pool } from 'pg';
import { postgresSchedulerLeaseStore, startDevServer } from '@fabric-harness/node';

const pool = new Pool({ connectionString: process.env.DATABASE_URL });

await startDevServer({
  scheduler: {
    leaseStore: postgresSchedulerLeaseStore(pool),
    leaseMs: 60_000,
  },
});

The active lease renews while the job runs. An occurrence claim remains long enough to prevent a late or restarted replica from replaying the same cron tick. memorySchedulerLeaseStore() is the single-process default and is useful for tests, not multi-replica coordination.

Generated Databricks Apps bind scheduled work to the App principal's stable system tenant. Lakebase-backed Apps also wire the Postgres lease/cursor store and use run-once catch-up automatically. Without Lakebase, scheduling is process-local, uses skip, and should remain single-replica.

Each claimed Node occurrence is admitted as a durable finite run. It receives a run receipt, can be inspected through /runs/:runId, and deduplicates across replicas using the schedule expression and scheduled timestamp. If an occurrence is more than Node's maximum timer delay away, the scheduler re-arms the same occurrence at each timer boundary; it does not execute the job early.

Scheduled Databricks App runs belong to the App principal's tenant. A signed-in user's approval discovery route therefore cannot see a request raised inside that run. For human-gated schedules, persist a proposal for a later user-owned interaction or use a trusted decision bridge that retains the exact session/request receipt and resolves it through the stored approval boundary. The Buzz bridge supplies the transport correlation primitives; the application still owns identity, membership, expiry, authorization, and compare-and-swap checks.

Cloudflare Cron Triggers

fh build --target cloudflare discovers every schedule expression, writes it to wrangler.jsonc under triggers.crons, and emits a Worker scheduled() handler. Each occurrence gets a deterministic run ID and executes through the same finite-job function as POST /jobs/:name. Inspect it with GET /runs/:runId and read offset events from GET /runs/:runId/events?offset=0. Cloudflare serializes duplicate deliveries for that run ID through its Durable Object and returns the existing run instead of executing twice.

Cron expressions use the target runtime's timezone rules: Node accepts an explicit IANA timezone; Cloudflare Cron Triggers run in UTC. During DST changes, cron-parser advances missing wall-clock times to the next valid instant and executes a repeated wall-clock occurrence once.