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

# JobSpec

> The forge/v2 payload the CLI builds and the server admits — for reading a rejection, or writing your own tooling.

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
sf submit my-grpo --profile h200:8
cat .starforge/jobspec.json
```

You do not write a JobSpec by hand. `sf submit` builds one into the job package. Read this page when
you are debugging a rejected submission, writing a tool that submits, or working through MCP.

`apiVersion` must be `forge/v2` and `kind` must be the training kind the SDK ships
(`KIND_TRAINING`). Any other value is refused on both the client and the server, before anything is
scheduled.

## Shape

```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
  "apiVersion": "forge/v2",
  "kind": "TrainingJob",
  "metadata": {
    "name": "run-id-segment",
    "project": "my-lab",
    "owner": "",
    "labels": {}
  },
  "spec": {
    "recipe": { "name": "nemo-rl/grpo", "version": "…", "digest": "sha256:…" },
    "source": { "exp": "experiments/my-grpo" },
    "framework": {
      "kind": "nemo-rl",
      "version": "0.7.0",
      "runtime_id": "…",
      "image": "",
      "observability_url": ""
    },
    "resources": {
      "pools": [{ "name": "train", "series": "h200", "nodes": 1, "gpus_per_node": 8 }],
      "roles": { "trainer": "train" }
    },
    "model": { "base": "", "init_from": "run/<id>/checkpoint" },
    "data": {
      "train": { "dataset": "alice/gsm8k-zh@v2", "path": "train.parquet" },
      "corpus": "alice/handbook@v1"
    },
    "hyperparams": { "policy.optimizer.kwargs.lr": 1e-6 },
    "lifecycle": { "on_success": ["export"] },
    "plugins": [{ "id": "alice/opsd-patch", "version": "1.2.0", "digest": "sha256:…" }]
  },
  "provenance": {
    "git_commit": "…",
    "git_dirty": false,
    "config_sha": "…",
    "core_version": "0.3.2"
  }
}
```

`kind` in code is the constant `KIND_TRAINING`; if a dump shows a different string, match the SDK you installed, not this sketch.

## Field rules

Every rule here exists because a submission was rejected for it. They are grouped by the field, not
by how surprising they are.

| Field                              | Rule                                                                                                                                             |
| ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `metadata.owner`                   | Whatever the client sends is ignored. The server stamps the authenticated account                                                                |
| `spec.source.entrypoint`           | Must be empty. The recipe owns the entrypoint; use a `custom/custom` recipe to run your own command                                              |
| `spec.framework.image`             | The optional `--image` override. Required for custom jobs. The registry host must be on the deployment's allowlist                               |
| `spec.framework.observability_url` | Required when the recipe declares `external` observability, and forbidden when it declares `platform`                                            |
| `spec.model.init_from`             | `run/<run_id>/<kind>`, optionally `@step=<N>`                                                                                                    |
| `spec.data.corpus`                 | `<owner>/<name>[@version]`. The platform mounts it and sets `DOCS_DIR`. A hardcoded host path would skip the access check, so it is not accepted |
| `spec.resources.pools`             | Must be non-empty. GPU count is `nodes * gpus_per_node` per pool, summed across pools for the quota check                                        |
| `spec.plugins[].id`                | `<owner>/<name>`. Digests start with `sha256:`. Two plugins that would unpack to the same leaf directory cannot be combined                      |

## Handshake

Admission compares four things against the server's catalog: the recipe name, its version, its
digest, and `framework.runtime_id`. Any mismatch is refused.

That is what `sf recipe status` reports and `sf recipe upgrade` fixes. Provenance `core_version`
must also satisfy the `requires.core` range on the lock — an old CLI submitting against a newer
catalog is refused with both versions named.
