# 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 set.

A run that names no version gets the agent's latest version. Once a new version has done better on your test set, as [Compare a version on a test set](https://docs.kindgi.com/v0.1.10/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.1.10/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.1'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 gate:** a comparison of the version on your test set, 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.1'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 promotion:

```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.1.10/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 promotion, 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

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 promotion, 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.1.10/guides/evals/live-versions-reference.md#the-history) shows. [Rollbacks and unpins](https://docs.kindgi.com/v0.1.10/guides/evals/live-versions-reference.md#rollbacks-and-unpins) lists what each refuses.

## Gate a release

A **gate policy** says what a promotion must show before a version goes live for a scope: a recent comparison on your test set, enough judged 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 promotion is checked by the most specific one covering its scope, at its latest version.

### Set a gate

A gated scope 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 set, 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.

The tenant's promotions of `acme.order-desk` now go through the gate. [Gate policies](https://docs.kindgi.com/v0.1.10/guides/evals/live-versions-reference.md#gate-policies) has what each part asks, and who may publish one.

### Check before you promote

`--check` asks the gate 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 comparison of 1.3.0, where `<comparison-id>` is its eval run's id. It prints what the gate 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 comparison finished 0.013 hours ago; the policy accepts up to 168.", … },
    { "name": "baselineIsLive", "passed": true, "message": "The baseline is acme.order-desk 1.0.0, the version live for the scope.", … },
    { "name": "metric.weightedYesShare.minCandidate", "passed": true, "message": "The weighted yes share 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 comparison: name one with `evalRunId` (a finished comparison eval run of this version).`` when there's no `--eval-run`.

### Promote through the gate

Promote it 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 promotion or the gate's refusal:

- **When the policy has no `approvals`,** a promotion 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 gate refused acme.order-desk 1.3.0: The weighted yes share rests on 3 judged cases for the candidate; the policy needs at least 20. …`, and kept in the history as `refused`.

### 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.1.10/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 promotion's `approvalId`. The version goes live, and the promotion 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 gates with `approvals` need `"separateApprover": false`. With [people of their own](https://docs.kindgi.com/v0.1.10/deploy/people-and-keys.md), register a second reviewer instead.

### Promote from your app

Your app checks the gate, then promotes. Ask your coding agent: "Check the release gate for my new agent version, and promote it unless the gate refuses." For reference, the script looks like this, with the comparison's id 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 gate's outcome, then the promotion 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.1.10/guides/evals/live-versions-reference.md)
- [Compare a version on a test set](https://docs.kindgi.com/v0.1.10/guides/evals/compare-an-agent-version.md)
- [Decide an approval](https://docs.kindgi.com/v0.1.10/guides/approvals/decide-an-approval.md)
- [Agent and tool versions](https://docs.kindgi.com/v0.1.10/guides/agents/agent-and-tool-versions.md)
