---
name: harbor-jobs
user-invocable: false
description: "Provides reusable Harbor job authoring and invocation. Use defineJob in a TypeScript file published with hrbr exec -f when the user wants a typed reusable function callable from hrbr.jobs, apps, workflows, or other job surfaces. Jobs are immutable versions and do not run autonomously when created."
---

# Harbor Jobs

## Use When

- The user wants reusable typed behavior rather than a one-off exec.
- A route, workflow, or future exec should call the same backend function.
- The function needs workspace-scoped runtime primitives such as hrbr.cache, hrbr.db, hrbr.storage, hrbr.ai, hrbr.tools, or plugin calls.
- The app needs plugin data. Publish a job that calls plugins, then let the app call that job.
- The user wants a job to be triggerable by a schedule or webhook. Triggerable
  jobs are normal published jobs with a static `triggers` manifest.

## Do Not Use When

- The user only needs a one-off probe. Use harbor-exec.
- The user wants a routed UI or public URL. Use harbor-apps, optionally backed by jobs.
- The user wants to schedule or loop work. Creating a job only creates an immutable callable version; it does not run by itself.
- The user wants app/job lifecycle functions from inside run. Lifecycle authoring happens through top-level defineJob source published with hrbr exec -f and inspect/control-plane reads.

## Canonical Authoring

Write one top-level defineJob call in a TypeScript file. Do not import it. Do not declare runtime, worker, or provider fields.

    defineJob({
      name: "summarize-message",
      description: "Summarize one message and cache the last input.",
      input: {
        message: "string",
        maxWords: "number?",
      },
      output: {
        ok: "boolean",
        summary: "string",
      },
      async run(input) {
        await hrbr.cache.set("jobs:summarize:last", { message: input.message }, 300);
        const summary = await hrbr.ai.run("@cf/meta/llama-3-8b-instruct", {
          prompt: "Summarize in " + (input.maxWords ?? 20) + " words: " + input.message,
        });
        return { ok: true, summary: String(summary.response ?? summary) };
      },
    });

Publish and invoke:

    hrbr exec -f ./summarize-message.job.ts
    hrbr exec 'return await hrbr.jobs.summarizeMessage({ message: "hello", maxWords: 12 })'

Re-running the same job name creates the next immutable version. hrbr.jobs.someJob(input) uses the promoted ready version unless an app route pins a specific version at publish time.

## Triggerable Jobs

Triggers do not run arbitrary jobs. A trigger can bind only to a ready job
version whose published source declares a static `triggers` array. The
authoring compiler stores that array as the job version's `trigger_manifest`.

Current trigger source kinds are:

- `schedule.cron` for recurring schedules.
- `schedule.once` for one-time schedules.
- `webhook.http` for HTTP webhooks. Provider names are not trigger kinds.

Minimal cron-triggerable job:

    defineJob({
      name: "daily-summary",
      description: "Run from a Harbor cron trigger.",
      input: {
        delivery_id: "string?",
        trigger_id: "string?",
        source: "string?",
        source_delivery_id: "string?",
        scheduled_for: "string?",
      },
      output: { ok: "boolean" },
      triggers: [
        {
          source_kind: "schedule.cron",
          event: "tick",
          input_mapping: {
            mode: "source_event",
            schema: "harbor.schedule.v1",
          },
          idempotency: {
            key: ["source_delivery_id"],
            ttl_seconds: 604800,
          },
          concurrency: {
            scope: "trigger",
            key: ["trigger_id"],
            limit: 1,
            overflow: "queue",
          },
          retry: {
            max_attempts: 3,
            backoff: "exponential",
          },
        },
      ],
      async run(input) {
        return { ok: true };
      },
    });

Manifest rules:

- Use `source_kind`, not `kind`, in trigger bindings.
- Use only `schedule.cron`, `schedule.once`, or `webhook.http`.
- Keep `triggers` fully static. Variables, spreads, imports, and computed
  trigger objects are rejected.
- `input_mapping` is required. Use `mode: "source_event"` when the trigger
  delivery should become the job input.
- If `concurrency` is present, include `key`, `limit`, and `overflow`.
- Webhook bindings may set `event`, but provider-specific names belong in the
  webhook source configuration, not in `source_kind`.

Publish, inspect, and test compatibility:

    hrbr exec -f ./daily-summary.job.ts
    hrbr inspect 'return await hrbr.jobs.inspect({ name: "daily-summary" })'
    hrbr inspect 'return await hrbr.triggers.inspect({
      source: { kind: "schedule.cron", cron: "0 9 * * *" },
      target: { job: "daily-summary", version: "v1" },
      activation: { name: "Daily summary" }
    })'

If trigger inspect says the job does not declare a compatible binding, publish a
new job version whose `triggers` array contains the requested `source_kind`.
Do not try to fix it with cron syntax or trigger settings.

## Schema Shortcuts

Shortcut literals are for the LLM-facing authoring path:

    input: {
      message: "string",
      count: "number?",
      ok: "boolean",
      metadata: "unknown?",
    }

Rules:

- string, number, boolean, and unknown are scalar fields.
- A question-mark suffix makes a property optional.
- Nested literal objects become JSON Schema objects with additionalProperties false.
- Full JSON Schema objects are accepted when they include a type field.
- Keep schemas literal. Dynamic schema construction is rejected.

## Runtime Contract

Inside run(input), use the runtime primitives directly:

    await hrbr.storage.put("jobs/report.json", JSON.stringify({ ok: true }), {
      content_type: "application/json",
    });
    await hrbr.cache.set("jobs:last", { ok: true }, 300);
    const rows = await hrbr.db.query("select id from notes limit 10", []);
    const tools = await hrbr.tools.search("create issue");

For plugin calls inside a job, use plugins.call(namespace, tool, input). The job authoring compiler infers the plugins capability when plugins.call appears in source.

    defineJob({
      name: "exa-search-job",
      input: { query: "string" },
      output: { count: "number", titles: "unknown" },
      async run(input) {
        const result = await plugins.call("exa-mcp", "web_search_exa", {
          query: input.query,
          numResults: 5,
        });
        const rows = Array.isArray(result?.results) ? result.results : [];
        return { count: rows.length, titles: rows.map((row) => row.title).slice(0, 5) };
      },
    });

Do not call direct plugin namespace globals inside job code unless the job runtime explicitly documents them. Use plugins.call for job plugin access.

Do not write hrbr.jobs.publish(...), hrbr.jobs.create(...), hrbr.apps.publish(...), or orbit.jobs.publish(...) inside job runtime code.

## Jobs Plus Apps

When an app needs connected plugin data, build it as two pieces:

1. A job that owns plugin calls and workspace work.
2. An app route that invokes the job or declares a job-backed route.

The app owns routing, access, HTML, forms, and browser behavior. The job owns reusable typed backend work.

Minimal route-job shape:

    deployApp({
      id: "plugin-report",
      access: "public",
      jobs: {
        search: { name: "exa-search-job" },
      },
      routes: {
        "POST /api/search": {
          input: "json",
          output: "json",
          job: "search",
        },
      },
    });

## More Detail

Read only what you need:

- references/job-runtime-contract.md: runtime namespaces, plugin calls, schemas, errors, and versioning.
- references/triggerable-jobs.md: schedule/webhook trigger manifests, templates, and compatibility checks.
- references/job-app-composition.md: app plus job combinations, including plugin-backed apps.
- assets/templates/plugin-search-job.ts: job that calls a plugin.
- assets/templates/storage-report-job.ts: job that writes an HTML artifact to hrbr.storage.
- assets/templates/app-backed-job.ts: backing job intended for a deployApp route.
- assets/templates/trigger-cron-summary-job.ts: cron-triggerable job template.
- assets/templates/trigger-once-reminder-job.ts: one-time schedule trigger job template.
- assets/templates/trigger-webhook-echo-job.ts: webhook-triggerable job template.

## Verification

    hrbr exec -f ./my-job.job.ts
    hrbr inspect 'return await hrbr.jobs.inspect({ name: "my-job" })'
    hrbr exec 'return await hrbr.jobs.myJob({ ... })'

For jobs that call plugins, verify with a real ready source and a small input. If schema errors occur, inspect the tool first:

    hrbr inspect 'return await hrbr.tools.search({ query: "exa web search", source: "exa-mcp" })'

## Done Criteria

- The file contains exactly one top-level defineJob call.
- Publish happened through hrbr exec -f and produced a run id.
- The job is inspectable and invokable through hrbr.jobs.camelName(input).
- Plugin-using jobs use plugins.call and were tested against a ready source.
- Jobs return compact typed data, storage keys, or URLs; they do not return giant blobs.
- No lifecycle API was declared inside run.
