> ## Documentation Index
> Fetch the complete documentation index at: https://docs.harborframework.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Submitting Jobs

> Launch a remote rollout with POST /job-submit and check its status

`POST /job-submit` launches a remote rollout.

The examples below use `$BASE` and `$KEY`. Get a key from
[Harbor Hub API Key](/core-concepts/hosted-harbor/api-key).

```bash theme={"system"}
export BASE=https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1
export KEY=sk-harbor-...
```

Request headers:

```http theme={"system"}
Authorization: Bearer sk-harbor-...
Content-Type: application/json
Idempotency-Key: <unique-retry-key>
```

The `Idempotency-Key` header is **required**. Requests without it are rejected, and the key may be
at most 200 characters. Reusing a key returns the job that key already created instead of launching
a second one, so generate a fresh key per launch and reuse it only when retrying that same launch.
If `Idempotency-Key` is reused, the server will return the previous launch URL even if the config changes.

The top level of the request body takes these fields:

| Field                  | Required | Notes                                                                                                                                       |
| ---------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `config`               | yes      | The job configuration                                                                                                                       |
| `organization`         | no       | Name of the organization that will own the job. Defaults to your personal org                                                               |
| `job_secrets`          | no       | One-off environment variable name to secret value. Added just for this job, and injected only into agents whose `secrets` sub-field name it |
| `registry_credentials` | no       | Registry host to stored credential name or ID `"us-east1-docker.pkg.dev": "registry-cred-stored-name"`                                      |
| `dry_run`              | no       | Validate and resolve everything without creating a job. Defaults to `false`                                                                 |

`job_secrets` and `registry_credentials` are siblings of `config`, not fields inside it. This is
deliberate: it keeps plaintext secrets out of the job config that gets stored and replayed. Placing
either one inside `config` is rejected. Upon submission these credentials are routed through an encryptor and decrypted only at runtime.
Plaintext secrets are never stored on our platform.

Here is a launch using a registry dataset and a built-in agent:

```json theme={"system"}
{
  "config": {
    "job_name": "I-love-harbor",
    "agents": [
      {
        "name": "terminus-2",
        "model_name": "openai/gpt-5.6-luna",
        "secrets": ["OPENAI_API_KEY"]
      }
    ],
    "datasets": [
      {
        "name": "harbor/hello-world",
        "ref": "latest",
        "n_tasks": 5
      }
    ],
    "n_attempts": 1,
    "n_concurrent_trials": 20
  },
  "job_secrets": {
    "OPENAI_API_KEY": "sk-..."
  },
  "dry_run": false
}
```

A successful launch returns the job and its viewer URL:

```json theme={"system"}
{
  "job_id": "00000000-0000-0000-0000-000000000001",
  "job_name": "I-love-harbor",
  "viewer_url": "https://hub.harborframework.com/jobs/00000000-0000-0000-0000-000000000001",
  "status_url": "https://hub.harborframework.com/jobs/00000000-0000-0000-0000-000000000001",
  "n_trials": 1,
  "owner_org": { "id": "...", "name": "..." },
  "agent_sources": []
}
```

`owner_org` reports which organization ended up owning the job, and `agent_sources` echoes any
resolved custom agent sources including their pinned commits. On a dry run no job is created, so
`job_id` comes back `null`, but every source is still resolved and validated.

To submit a request stored in `launch.json`:

```bash theme={"system"}
BASE="https://ofhuhcpkvzjlejydnvyd.supabase.co/functions/v1"
KEY="sk-harbor-..."

curl -sS -X POST "$BASE/job-submit" \
  -H "Authorization: Bearer $KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d @launch.json
```

## Job Config

`config` mirrors the `JobConfig` the CLI sends, so unrecognized keys pass through untouched.
The fields the API validates directly are:

| Field                 | Required | Notes                                                                                                                                                         |
| --------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `agents`              | yes      | At least one agent                                                                                                                                            |
| `tasks`               | no       | Individual tasks                                                                                                                                              |
| `datasets`            | no       | Registry or git repo datasets                                                                                                                                 |
| `job_name`            | no       | Defaults to a UTC timestamp such as `2026-08-13__14-30-00` (this timestamp can be super annoying to look through later. So seriously consider setting a name) |
| `n_attempts`          | no       | Between 1 and 10. Defaults to 1                                                                                                                               |
| `n_concurrent_trials` | no       | Between 1 and 1000. Defaults to 4                                                                                                                             |
| `credential_mode`     | no       | `gateway` (default) or `direct`. `gateway` uses credential proxying. `direct` directly injects decrypted secrets into the task container                      |
| `retry`               | no       | `include_exceptions` and `exclude_exceptions` lists                                                                                                           |

You need at least one of `tasks` or `datasets`, and together they must resolve to at least one task.
The total number of trials is `n_attempts × tasks × agents`, and must land between 1 and 50,000.

On each agent, `name` is the Harbor agent type, `model_name` is the model it uses, and `secrets` is
the list of credentials that agent receives. Agent versions installed from a package manager are
pinned when the job is submitted, so every trial in a job installs the same release even if the job
sits queued across an upstream publish.

## Agent Secrets

Credentials are scoped per agent, not per job. Every agent carries its own `secrets` list naming the
environment variables it receives, so two agents in the same launch can be given different keys.

```json theme={"system"}
{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY", "HF_TOKEN"]
}
```

Each name must look like an environment variable, matching `^[A-Z][A-Z0-9_]{0,63}$`, and
at most 64 of them may be selected per-agent. Secrets are matched to their values either
from stored organization secrets, or supplied through the launch's `job_secrets`. When there is
a secret supplied through `job_secrets` and stored in the organization secrets,
the `job_secrets` submission wins. A selected name backed by neither carries no value of its
own.

For example, if I launch a job with the following config snippet:

```json theme={"system"}
{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
"job_secrets": {}
```

and `ANTHROPIC_API_KEY` is not set on my Harbor Hub organization, the variable `ANTHROPIC_API_KEY=None`
would be injected into the agent's task environment. If `ANTHROPIC_API_KEY` is set on my Harbor Hub
organization, it will be appropriately injected.

If I instead launch with the following config snippet:

```json theme={"system"}
{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
"job_secrets": {"ANTHROPIC_API_KEY": "sk-ant-job-secret"}
```

Then the value provided in `job_secrets` (or a proxy) will be injected into the agent env
regardless of whether or not the same credential name is stored on my Harbor Hub organization.

You should send the `secrets` field for every agent.

```json theme={"system"}
{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "secrets": ["ANTHROPIC_API_KEY"]
}
```

| `secrets`              | What the agent receives                              |
| ---------------------- | ---------------------------------------------------- |
| `[A, list, of, names]` | Exactly those names, as far as each one resolves     |
| `[]`                   | Nothing. The agent starts with no credentials at all |

The `oracle` and `nop` agents need no inference credential and can be launched with `"secrets": []`.

Under the default `gateway` credential mode, a selected credential for `openai`, `anthropic`,
`openrouter`, `xai`, or `gemini` arrives as a proxy capability rather than your real
key, injected under its own name alongside the provider's standard variables so an agent's usual
lookup finds it. Everything else you select is injected
under its own name with its real value. Credentials for `vercel_ai_gateway` and `devin`
require `"credential_mode": "direct"`; in gateway mode they are dropped.

Secrets should not be placed in an agent's `env`. That is only for nonsensitive environment variables.

```json theme={"system"}
{
  "name": "claude-code",
  "model_name": "anthropic/claude-opus-4-1",
  "env": [], // secrets do NOT go here
  "secrets": ["ANTHROPIC_API_KEY"] // secret names go HERE
}
"job_secrets": {"ANTHROPIC_API_KEY": "sk-ant-job-secret"} // secret {"key": value} go HERE
```

## Tasks

Use `tasks` for individual tasks. Each entry is either a registry task or a git task, never both.

A registry task takes a `name` in `org/name` form and an optional `ref`, which is a version ref like
`"latest"`, or a semantic version string.

A git task takes a `git_url` and a `path`, where `path` is the task directory relative to the repository
root and is **required**. Pin the revision with either `git_ref` (a branch, tag, or SHA) or
`git_commit_id` (a resolved 40-character SHA), but not both. Git tasks use those fields rather than
`ref`, and local filesystem paths cannot be used for remote rollouts.

```json theme={"system"}
{
  "config": {
    "job_name": "individual-tasks",
    "agents": [
      {
        "name": "terminus-2",
        "model_name": "openai/gpt-5-mini",
        "secrets": ["OPENAI_API_KEY"]
      }
    ],
    "tasks": [
      {
        "git_url": "https://github.com/owner/private-tasks.git",
        "git_ref": "main",
        "path": "tasks/my-task"
      },
      {
        "name": "terminal-bench/torch-tensor-parallelism",
        "ref": "latest"
      }
    ]
  },
  "job_secrets": {
    "OPENAI_API_KEY": "sk-..."
  },
  "dry_run": false
}
```

Private GitHub repositories have to be connected or shared through the profile settings flow before
they can be used as a task or agent source. TODO: add pics here.

## Datasets

Use `datasets` for registry or git repo datasets.

A registry dataset takes a `name` in `org/name` form and either `ref` or `version`, but never both.
You can narrow it with `n_tasks` to cap how many tasks are drawn, and with the `task_names` and
`exclude_task_names` glob patterns.

A git repo dataset uses Harbor's `repo` source string, the same grammar as `harbor run --repo`. That
accepts `org/name`, `github.com/org/name`, a full https or ssh URL, or a GitHub `/tree/<ref>/<subdir>`
URL, each with an optional trailing `@<branch|tag|sha>`. The optional `path` is the tasks directory
relative to the repository root, which Harbor defaults to `tasks`.

```json theme={"system"}
{
  "datasets": [
    {
      "repo": "my-org/my-tasks@main",
      "path": "tasks",
      "n_tasks": 5
    }
  ]
}
```

Git repo datasets pin their revision inside the repo string, so they do not take `ref` or `version`.
They also do not accept `git_url`, `git_ref`, or `git_commit_id` — those belong to `tasks`.

Whatever a `latest` or a tag resolves to is pinned into the stored config, so a saved job records the
concrete version that actually ran rather than a moving reference.

## Job Secrets

`job_secrets` is a flat map of environment variable name to value:

```json theme={"system"}
{ "OPENAI_API_KEY": "sk-..." }
```

Names must be uppercase and start with a letter, values must be non-empty and at most 16,384
characters, and you can send at most 50 entries. Values are encrypted before storage and never
returned.

Secret-looking environment variables found inside `config` are automatically moved into this
encrypted channel before anything is saved, so plaintext never lands in a stored config. If the same
name appears in both places, the explicit `job_secrets` entry wins.

Sending a value here makes it available to the job; it does not decide which agent gets it. Name it
in that agent's [`secrets`](/core-concepts/hosted-harbor/submitting-jobs#agent-secrets) too, unless you are relying on the omitted-`secrets`
fallback for the model's canonical key.

You can also store a key once as a hosted secret and leave it out of the launch entirely. Stored
secrets are still selected the same way, by naming them in `secrets`, and are read from the
organization that owns the job.

`GET /job-status` returns trial counts for one or more jobs.

```bash theme={"system"}
curl -sS "$BASE/job-status?job_id=<uuid>" \
  -H "Authorization: Bearer $KEY"
```

Pass `job_id` once per job to check several at a time, or a comma-separated `job_ids` list. Add
`force_combined=true` to get the combined overview shape even for a single job. The same request
works as a `POST` with a `{"job_ids": [...], "force_combined": false}` body.

A single-job response reports `pending`, `running`, `completed`, `failed`, `canceled`, and `total`.
