# Release an agent version

> Make a version live for the whole tenant, an org, a project or a segment of its runs, see which version a run gets and why, roll back or unpin, and read the history.

This guide makes an agent version live where you choose, checks which version a run gets, rolls back, and gates a release behind your test cases.

A run that names no version gets the agent's latest version. Once a new version has done better on your test cases, as [Try a version on the test cases](https://docs.kindgi.com/v0.3.1/guides/evals/compare-an-agent-version.md) shows, you can make it **live** where you choose: the whole tenant, an org, a project, or a segment of a project's runs, such as one company's. Runs there use it from the next run, nothing changes anywhere else, and you can roll back at once. [Live versions reference](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md) has every rule, refusal and console page.

## Before you start

- **An agent with more than one version,** registered on a runtime such as `kindgi dev`, with Kindgi 0.2's CLI. This guide uses `acme.order-desk`, with versions 1.0.0, 1.1.0, 1.2.0 and 1.3.0.
- **A project's id,** for a project or a segment: `kindgi projects list` lists them.
- **For a release gate policy:** a run of the version on your test cases, and a second reviewer to approve.
- **For the app's half:** Node 22.12 or later, or Python 3.11 or later, with Kindgi 0.2's client, and `KINDGI_API_URL` and `KINDGI_API_TOKEN` set.

## Make a version live

A live version is what a run that names no version gets.

Make 1.0.0 live for the whole tenant:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Python**

  ```sh
  uv run kindgi agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Java**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Scala**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

* `acme.order-desk 1.0.0` is the agent and the version.
* `--tenant` is the scope: the whole tenant.
* `--reason` is kept on the record.

It prints the request to make it live:

```json
{
  "id": "ea4e7c15-7990-41b6-b153-0847a83f2e9d",
  "agentId": "acme.order-desk",
  "scope": { "kind": "tenant" },
  "action": "promote",
  "fromVersion": null,
  "toVersion": "1.0.0",
  "requestedBy": { "kind": "user", "id": "5c0a7e11-0000-4000-8000-0000000000aa" },
  "reason": "1.0.0 is the version we checked",
  "createdAt": "2026-10-06T18:46:15.125Z"
}
```

From the next run, a run that names no version gets 1.0.0, and says why:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --input='{"userMessage":"Where is order A-1002?"}'
  ```

```json excerpt
  "agent": {
    "id": "acme.order-desk",
    "version": "1.0.0",
    "conversationId": "4af092ea-5e8e-42f1-832e-b7ba1ed6e3d8",
    "via": "live",
    "liveScope": { "kind": "tenant" }
  },
```

`via: "live"` says it got the live version of the scope in `liveScope`. [How a run gets its version](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md#how-a-run-gets-its-version) lists every `via`. A version only goes live this way: publishing a newer version doesn't change what a scope with a live version runs.

## Make it live for some runs only

A narrower scope tries a version with some runs first. The most specific live version wins: a **segment** path in a project, then the **project**, the project's **org**, the **tenant**, and then the latest version. A segment path is one or more `key:value` steps, coarse to fine, that a run is started with: `company:acme`, or `company:acme` then `role:counsel`.

Try 1.1.0 with acme's runs only:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents promote acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"
  ```

- **Python**

  ```sh
  uv run kindgi agents promote acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"
  ```

- **Java**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"
  ```

- **Scala**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"
  ```

* `--project=<project-id>` is the project, where `<project-id>` is its id.
* `--segment=company:acme` is the segment path in it.
* `--org=<org-id>` names an org instead.

It prints the request to make it live, with the segment as its `scope`.

A run started with that segment gets 1.1.0:

- **TypeScript**

  ```sh
  pnpm exec kindgi runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Python**

  ```sh
  uv run kindgi runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Java**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'
  ```

- **Scala**

  ```sh
  ./kindgiw runs start --agent=acme.order-desk --project=<project-id> --segment=company:acme --input='{"userMessage":"Where is order A-1002?"}'
  ```

```json excerpt
  "agent": {
    "id": "acme.order-desk",
    "version": "1.1.0",
    "conversationId": "d35be284-99c0-4437-9186-b951386db8b3",
    "via": "live",
    "liveScope": {
      "kind": "segment",
      "projectId": "367e1657-8067-4a1d-8ec4-bfa3c5fc5865",
      "path": [{ "key": "company", "value": "acme" }]
    }
  },
```

A run with `--segment=company:globex` has no live version of its own, so it gets the tenant's, 1.0.0. Without `--project`, a run is in its agent's own project, which is Default for what `kindgi deploy` registers.

## See which version a run would get

`kindgi agents live` answers without starting a run:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents live acme.order-desk --project=<project-id> --segment=company:acme
  ```

- **Python**

  ```sh
  uv run kindgi agents live acme.order-desk --project=<project-id> --segment=company:acme
  ```

- **Java**

  ```sh
  ./kindgiw agents live acme.order-desk --project=<project-id> --segment=company:acme
  ```

- **Scala**

  ```sh
  ./kindgiw agents live acme.order-desk --project=<project-id> --segment=company:acme
  ```

```json
{
  "agentId": "acme.order-desk",
  "version": "1.1.0",
  "via": "live",
  "liveScope": {
    "kind": "segment",
    "projectId": "367e1657-8067-4a1d-8ec4-bfa3c5fc5865",
    "path": [{ "key": "company", "value": "acme" }]
  }
}
```

`kindgi agents live-versions acme.order-desk` lists every scope with a live version, and since when.

## From your app

Your app can do the same. Ask your coding agent: "I want to make a new version of my agent live for one company's runs, and check which version a run gets." For reference, the script looks like this, with the project's id as its argument:

- **TypeScript**

  `release.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  const projectId = process.argv[2];
  const acme = [{ key: 'company', value: 'acme' }];

  // Make 1.1.0 live for acme's runs in this project.
  await kindgi.agents.promotions.create('acme.order-desk', {
    version: '1.1.0',
    scope: { kind: 'segment', projectId, path: acme },
    reason: 'Trying 1.1.0 with acme first',
  });

  // Which version would a run there use, and why?
  const live = await kindgi.agents.live.resolve('acme.order-desk', { projectId, segments: acme });
  console.log(live.version, live.via, live.liveScope?.kind);

  // A run that names no version gets it.
  const run = await kindgi.runs.start({
    agent: 'acme.order-desk',
    projectId,
    segments: acme,
    input: { userMessage: 'Where is order A-1002?' },
  });
  console.log(run.agent?.version, run.agent?.via);
  ```

- **Python**

  `release.py`

  ```python
  import sys

  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  project_id = sys.argv[1]
  acme = [{"key": "company", "value": "acme"}]

  # Make 1.1.0 live for acme's runs in this project.
  kindgi.agents.promotions.create(
      "acme.order-desk",
      version="1.1.0",
      scope={"kind": "segment", "project_id": project_id, "path": acme},
      reason="Trying 1.1.0 with acme first",
  )

  # Which version would a run there use, and why?
  live = kindgi.agents.live.resolve("acme.order-desk", project_id=project_id, segments=acme)
  print(live.version, live.via, live.live_scope.kind if live.live_scope else None)

  # A run that names no version gets it.
  run = kindgi.runs.start(
      agent="acme.order-desk",
      project_id=project_id,
      segments=acme,
      input={"userMessage": "Where is order A-1002?"},
  )
  if run.agent is not None:  # set on an agent's runs
      print(run.agent.version, run.agent.via)
  ```

It prints the version a run there would get, then the version the run got:

```text
1.1.0 live segment
1.1.0 live
```

`kindgi.agents.live.list`, `rollback` and `unpin`, and `kindgi.agents.promotions.list` and `get`, do what the commands on this page do.

## Roll back, or unpin

On a self-hosted runtime with one pack service, the version you roll back to runs only while your pack still has the tool versions it locked ([A self-hosted runtime runs what your pack has](https://docs.kindgi.com/v0.3.1/guides/agents/agent-and-tool-versions.md#a-self-hosted-runtime-runs-what-your-pack-has)).

A rollback puts a scope back on the version that was live there before, at once:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents rollback acme.order-desk --tenant --reason="1.2.0 answers worse"
  ```

- **Python**

  ```sh
  uv run kindgi agents rollback acme.order-desk --tenant --reason="1.2.0 answers worse"
  ```

- **Java**

  ```sh
  ./kindgiw agents rollback acme.order-desk --tenant --reason="1.2.0 answers worse"
  ```

- **Scala**

  ```sh
  ./kindgiw agents rollback acme.order-desk --tenant --reason="1.2.0 answers worse"
  ```

`--to=<version>` rolls back to another version instead. It prints the rollback:

```json excerpt
{
  "id": "7da674c5-c835-42c0-8cde-59571afd0915",
  "agentId": "acme.order-desk",
  "scope": { "kind": "tenant" },
  "action": "rollback",
  "fromVersion": "1.2.0",
  "toVersion": "1.0.0",
  …
}
```

`kindgi agents unpin` removes a scope's own live version. Its runs then use the scope above, or the latest version when nothing above is live:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents unpin acme.order-desk --tenant --reason="Back to the latest"
  ```

- **Python**

  ```sh
  uv run kindgi agents unpin acme.order-desk --tenant --reason="Back to the latest"
  ```

- **Java**

  ```sh
  ./kindgiw agents unpin acme.order-desk --tenant --reason="Back to the latest"
  ```

- **Scala**

  ```sh
  ./kindgiw agents unpin acme.order-desk --tenant --reason="Back to the latest"
  ```

Every request to make a version live, rollback and unpin is kept, with who asked and why. `kindgi agents promotions list acme.order-desk --table` lists them, as [The history](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md#go-live-history) shows. [Rollbacks and unpins](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md#rollbacks-and-unpins) lists what each refuses.

## Gate a release

A **release gate policy** says what a request to make a version live must show before the version goes live for a scope: a recent run on your test cases, enough corrected cases, figures at least this good, a clean replay and, if you want, a reviewer's approval. An agent has one policy per scope. A request to make a version live is checked by the most specific one covering its scope, at its latest version.

### Set a release gate policy

A scope with a release gate policy needs a live version of its own, so give the tenant one first:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Python**

  ```sh
  uv run kindgi agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Java**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

- **Scala**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.0.0 --tenant --reason="1.0.0 is the version we checked"
  ```

Ask your coding agent to write the policy:

```text
Before a new version of my agent goes live, I want it to have done well on my test cases, and a senior reviewer to approve it.
```

For reference, the policy it writes looks like this. This guide saves it as `gate.json`:

```json
{
  "comparison": { "maxAgeHours": 168 },
  "evidence": { "minCases": 3 },
  "metrics": [{ "name": "weightedYesShare", "minCandidate": 0.6, "maxDrop": 0.02 }],
  "replay": { "maxRefusedWrites": 0 },
  "approvals": { "role": "senior" }
}
```

Then publish it:

- **TypeScript**

  ```sh
  pnpm exec kindgi gate-policies publish acme.order-desk-release --policy-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"
  ```

- **Python**

  ```sh
  uv run kindgi gate-policies publish acme.order-desk-release --policy-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"
  ```

- **Java**

  ```sh
  ./kindgiw gate-policies publish acme.order-desk-release --policy-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"
  ```

- **Scala**

  ```sh
  ./kindgiw gate-policies publish acme.order-desk-release --policy-version=1.0.0 --agent=acme.order-desk --tenant --spec=@gate.json --description="What a release of the order desk must show"
  ```

* `acme.order-desk-release` is the policy's id, and `--policy-version` its version.
* `--agent` and `--tenant` are the agent and the scope it gates.
* `--spec=@gate.json` reads the policy from the file.

Requests to make `acme.order-desk` live anywhere in the tenant now go through the release gate policy, unless a narrower scope has one of its own. [Release gate policies](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md#release-gate-policies) has what each part asks, and who may publish one.

### Check before you make it live

`--check` asks the release gate policy without recording anything:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --check
  ```

- **Python**

  ```sh
  uv run kindgi agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --check
  ```

- **Java**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --check
  ```

- **Scala**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --check
  ```

`--eval-run=<comparison-id>` names the run of 1.3.0 on the test cases, where `<comparison-id>` is its eval run's id. It prints what the policy would say:

```json excerpt
{
  "outcome": "needs-approval",
  "policy": { "id": "acme.order-desk-release", "version": "1.0.0" },
  "checks": [
    { "name": "comparison.freshness", "passed": true, "message": "The test run finished 0.013 hours ago; the policy accepts up to 168.", … },
    { "name": "baselineIsLive", "passed": true, "message": "The test run measured this version against the live version, acme.order-desk 1.0.0.", … },
    { "name": "metric.weightedYesShare.minCandidate", "passed": true, "message": "The weighted share marked right is 1; the policy needs at least 0.6.", … },
    …
  ],
  "approval": { "role": "senior", "count": 1, "separateApprover": true }
}
```

`gate-failed` lists each check that failed, such as ``This policy needs a test run: name one with `evalRunId` (a finished eval run that tried this version on the test cases).`` when there's no `--eval-run`.

`--check` exits 0 for `would-promote` and 3 for the other two, so a script can stop on the answer.

When the test cases have corrections that say what the run should have done, the summary of the run on the test cases also says how many the version passed, in `corrections` ([What it should have done](https://docs.kindgi.com/v0.3.1/guides/evals/compare-an-agent-version.md#what-it-should-have-done)). No release gate policy checks it, so name it in the `--reason`, such as `--reason="Passes 3 of 3 corrections"`.

### Make it live through the release gate policy

Ask to make it live the same way, without `--check`:

- **TypeScript**

  ```sh
  pnpm exec kindgi agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --reason="1.3.0 did as well on the order checks"
  ```

- **Python**

  ```sh
  uv run kindgi agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --reason="1.3.0 did as well on the order checks"
  ```

- **Java**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --reason="1.3.0 did as well on the order checks"
  ```

- **Scala**

  ```sh
  ./kindgiw agents promote acme.order-desk 1.3.0 --tenant --eval-run=<comparison-id> --reason="1.3.0 did as well on the order checks"
  ```

It prints the request, with its status, or the policy's refusal:

- **When the policy has no `approvals`,** a request that passes goes live at once, `"status": "promoted"`.
- **When it has,** it waits: `"status": "pending-approval"`, with an `approvalId`. The scope keeps its version until a reviewer approves.
- **When it fails,** nothing changes. It's refused with every failed check, such as `The release gate policy refused acme.order-desk 1.3.0: The weighted share marked right rests on 3 judged cases for this version; the policy needs at least 20. …`, and kept in the history as `refused`.

It exits 0 when the version is live, and 3 when it waits for an approval or the policy refused it.

To wait for the reviewer, add `--wait`. It names the approval it waits for and the command that approves it, then exits 0 once the version is live, or 3 when the approval is rejected or expires or the wait runs out: an hour, or the time `--wait=30m` gives. It exits 3 too when the request is superseded: it was approved, but something the check measured had changed by then, such as the version serving the scope or its release gate policy, so promote it again: the check runs anew. It exits 1 when it can't read the request 3 times in a row, after printing what it read last. Neither that nor Ctrl-C stops the request: once it's approved, the version goes live.

### Approve it

The request is an approval like any other, for a reviewer with the policy's role, as [Decide an approval](https://docs.kindgi.com/v0.3.1/guides/approvals/decide-an-approval.md) shows:

- **TypeScript**

  ```sh
  pnpm exec kindgi approvals complete <approval-id> --decision=approve --rationale="Checked the comparison"
  ```

- **Python**

  ```sh
  uv run kindgi approvals complete <approval-id> --decision=approve --rationale="Checked the comparison"
  ```

- **Java**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=approve --rationale="Checked the comparison"
  ```

- **Scala**

  ```sh
  ./kindgiw approvals complete <approval-id> --decision=approve --rationale="Checked the comparison"
  ```

`<approval-id>` is the request's `approvalId`. The version goes live, and the request is `promoted`. Whoever asked can't approve their own request:

```text
Approval <approval-id> was asked for by you: someone else must approve it
```

That holds unless the policy's `approvals` has `"separateApprover": false`. A deployment that one person uses with the API token has nobody else to approve, so its release gate policies with `approvals` need `"separateApprover": false`. With [people of their own](https://docs.kindgi.com/v0.3.1/deploy/people-and-keys.md), register a second reviewer instead.

### Make it live from your app

Your app checks the release gate policy, then makes the version live. Ask your coding agent: "Check the release gate policy for my new agent version, and make it live unless the policy refuses." For reference, the script looks like this, with the id of the run on the test cases as its argument:

- **TypeScript**

  `promote-gated.ts`

  ```ts
  import { createClient } from '@kindgi/sdk/client';

  const kindgi = createClient(); // KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  const evalRunId = process.argv[2]; // the comparison of 1.3.0
  const request = {
    version: '1.3.0',
    scope: { kind: 'tenant' as const },
    evalRunId,
    reason: '1.3.0 did as well on the order checks',
  };

  // What would the gate say? Nothing is recorded.
  const check = await kindgi.agents.promotions.check('acme.order-desk', request);
  console.log(check.outcome);
  for (const c of check.checks.filter((c) => !c.passed)) console.log(c.message);

  if (check.outcome !== 'gate-failed') {
    const promotion = await kindgi.agents.promotions.create('acme.order-desk', request);
    console.log(promotion.status, promotion.approvalId ?? '');
  }
  ```

- **Python**

  `promote_gated.py`

  ```python
  import sys

  from kindgi.client import Kindgi

  kindgi = Kindgi()  # KINDGI_API_URL, KINDGI_API_TOKEN, or the running kindgi dev
  request = {
      "version": "1.3.0",
      "scope": {"kind": "tenant"},
      "eval_run_id": sys.argv[1],  # the comparison of 1.3.0
      "reason": "1.3.0 did as well on the order checks",
  }

  # What would the gate say? Nothing is recorded.
  check = kindgi.agents.promotions.check("acme.order-desk", **request)
  print(check.outcome)
  for c in check.checks:
      if not c.passed:
          print(c.message)

  if check.outcome != "gate-failed":
      promotion = kindgi.agents.promotions.create("acme.order-desk", **request)
      print(promotion.status, promotion.approval_id or "")
  ```

It prints the release gate policy's outcome, then the request waiting for its approval:

```text
needs-approval
pending-approval fa6fbbcf-7b0d-4c70-b462-d6a78bbcfd11
```

`kindgi.gatePolicies.publish`, or `gate_policies.publish` in Python, and `kindgi.agents.gatePolicy.resolve`, or `agents.gate_policy.resolve`, do what `kindgi gate-policies publish` and `kindgi agents gate-policy` do.

## Next

- [Live versions reference](https://docs.kindgi.com/v0.3.1/guides/evals/live-versions-reference.md)
- [Try a version on the test cases](https://docs.kindgi.com/v0.3.1/guides/evals/compare-an-agent-version.md)
- [Decide an approval](https://docs.kindgi.com/v0.3.1/guides/approvals/decide-an-approval.md)
- [Agent and tool versions](https://docs.kindgi.com/v0.3.1/guides/agents/agent-and-tool-versions.md)
