# Release an agent version

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

This guide pins an agent version where you choose, checks which version a run gets, unpins, and gates a pin behind your test cases.

A run that names no version gets the agent's **Latest** in its project: the version the newest deployment into the project shipped, or the one a person published there in full, whichever came last. A version derived from an edit to its prompts or settings isn't Latest by itself, and neither is a higher number. Once a new version has done better on your test cases, as [Try a version on the test cases](https://docs.kindgi.com/v0.4.0/guides/improve/compare-an-agent-version.md) shows, you can **pin** it 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 unpin at once. [Pinned versions reference](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-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:** 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.

## Pin a version

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

Pin 1.0.0 for the whole tenant:

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

  ```sh
  ./kindgiw agents pin 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 pin request:

```json
{
  "id": "ea4e7c15-7990-41b6-b153-0847a83f2e9d",
  "agentId": "acme.order-desk",
  "scope": { "kind": "tenant" },
  "action": "pin",
  "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": "pinned",
    "pinnedScope": { "kind": "tenant" }
  },
```

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

**For an agency,** with a project for each client, pin in the client's project instead, with `--project=<project-id>`, as in [Pin it for some runs only](https://docs.kindgi.com/v0.4.0/guides/improve/release-an-agent-version.md#pin-it-for-some-runs-only) but without `--segment`. A tenant pin changes the agent at once for every client without a pin of its own. When a [release gate](https://docs.kindgi.com/v0.4.0/guides/improve/release-an-agent-version.md#gate-a-pin) holds a pin for an approval, the approval is in the pin's project: a tenant or org pin's is in no project, so people who review only their project's approvals don't see it.

## Pin it for some runs only

A narrower scope tries a version with some runs first. The most specific pinned version wins: a **segment** path in a project, then the **project**, the project's **org**, the **tenant**, and then the project's Latest. 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 pin 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 pin acme.order-desk 1.1.0 --project=<project-id> --segment=company:acme --reason="Trying 1.1.0 with acme first"
  ```

- **Java**

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

- **Scala**

  ```sh
  ./kindgiw agents pin 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 pin request, 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": "pinned",
    "pinnedScope": {
      "kind": "segment",
      "projectId": "367e1657-8067-4a1d-8ec4-bfa3c5fc5865",
      "path": [{ "key": "company", "value": "acme" }]
    }
  },
```

A run with `--segment=company:globex` has no pinned 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 the default project for what `kindgi deploy` registers.

## See which version a run would get

`kindgi agents pinned` answers without starting a run:

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

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

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

## From your app

Your app can do the same. Ask your coding agent: "I want to pin a new version of my agent 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' }];

  // Pin 1.1.0 for acme's runs in this project.
  await kindgi.agents.pinned.pin('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 pinned = await kindgi.agents.pinned.resolve('acme.order-desk', { projectId, segments: acme });
  console.log(pinned.version, pinned.via, pinned.pinnedScope?.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"}]

  # Pin 1.1.0 for acme's runs in this project.
  kindgi.agents.pinned.pin(
      "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?
  pinned = kindgi.agents.pinned.resolve("acme.order-desk", project_id=project_id, segments=acme)
  print(pinned.version, pinned.via, pinned.pinned_scope.kind if pinned.pinned_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 pinned segment
1.1.0 pinned
```

`kindgi.agents.pinned.list`, `unpin`, `history` and `getChange` (`agents.pinned.get_change` in Python) do what the commands on this page do.

## Unpin

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

- **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"
  ```

To go back to an earlier version, pin it again. On a self-hosted runtime with one pack service, that version 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.4.0/guides/agents/agent-and-tool-versions.md#a-self-hosted-runtime-runs-what-your-pack-has)).

Every request to pin a version and every unpin is kept, with who asked and why. `kindgi agents pin-history list acme.order-desk --table` lists them, as [The pin history](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md#pin-history) shows. [Unpins](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md#unpins) lists what each refuses.

## Gate a pin

A **release gate** says what a request to pin a version must show before the version is pinned 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 release gate per scope. A request to pin a version is checked by the most specific one covering its scope, at its latest version.

### Set a release gate

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

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

Ask your coding agent to write the release gate:

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

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

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

**For an agent whose job is a write,** such as sending a reply or placing an order, leave `replay` out and gate on `cases` instead, for example `"cases": { "minPassing": 3, "noNewFailures": true }`. [A replay](https://docs.kindgi.com/v0.4.0/guides/improve/compare-an-agent-version.md#what-a-replay-does) refuses a call that changes things unless the past run made the same call, with the same arguments, and a write the model words, such as a mail's body, almost never repeats word for word. Each refusal counts in `refusedWrites`, so `"maxRefusedWrites": 0` refuses every version. To keep `replay`, set `maxRefusedWrites`, and any other count the version's run on the test cases shows, to what you accept: those left out are 0.

Then publish it:

- **TypeScript**

  ```sh
  pnpm exec kindgi release-gates publish acme.order-desk-release --release-gate-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 release-gates publish acme.order-desk-release --release-gate-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 release-gates publish acme.order-desk-release --release-gate-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 release-gates publish acme.order-desk-release --release-gate-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 release gate's id, and `--release-gate-version` its version.
* `--agent` and `--tenant` are the agent and the scope it gates.
* `--spec=@gate.json` reads the release gate from the file.

Requests to pin `acme.order-desk` anywhere in the tenant now go through the release gate, unless a narrower scope has one of its own. An approval it asks for is in the project of the pin it holds: a pin in a client's project waits among that project's approvals. [Release gates](https://docs.kindgi.com/v0.4.0/guides/improve/pinned-versions-reference.md#release-gates) has what each part asks, and who may publish one.

### Check before you pin it

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

- **TypeScript**

  ```sh
  pnpm exec kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check
  ```

- **Python**

  ```sh
  uv run kindgi agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check
  ```

- **Java**

  ```sh
  ./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check
  ```

- **Scala**

  ```sh
  ./kindgiw agents pin acme.order-desk 1.3.0 --tenant --test-run=<test-run-id> --check
  ```

`--test-run=<test-run-id>` names the run of 1.3.0 on the test cases. It prints what the release gate would say:

```json excerpt
{
  "outcome": "needs-approval",
  "releaseGate": { "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 gate accepts up to 168.", … },
    { "name": "baselineIsPinned", "passed": true, "message": "The test run measured this version against the pinned version, acme.order-desk 1.0.0.", … },
    { "name": "metric.passRate.minCandidate", "passed": true, "message": "The weighted share marked right is 1; the gate needs at least 0.6.", … },
    …
  ],
  "approval": { "role": "senior", "count": 1, "separateApprover": true }
}
```

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

`--check` exits 0 for `would-pin` 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.4.0/guides/improve/compare-an-agent-version.md#what-it-should-have-done)). No release gate checks it, so name it in the `--reason`, such as `--reason="Passes 3 of 3 corrections"`.

### Pin through the release gate

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

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

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

- **When the release gate has no `approvals`,** a request that passes is pinned at once, `"status": "pinned"`.
- **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 refused acme.order-desk 1.3.0: The weighted share marked right rests on 3 corrected cases for this version; the gate needs at least 20. …`, and kept in the history as `refused`.

It exits 0 when the version is pinned, and 3 when it waits for an approval or the release gate 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 pinned, 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, so pin 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 is pinned.

### Approve it

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

- **TypeScript**

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

- **Python**

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

- **Java**

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

- **Scala**

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

`<approval-id>` is the request's `approvalId`. The version is pinned, and the request is `pinned`. 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 release gate's `approvals` has `"separateApprover": false`. A deployment that one person uses with the API token has nobody else to approve, so its release gates with `approvals` need `"separateApprover": false`. With [people of their own](https://docs.kindgi.com/v0.4.0/deploy/people-and-keys.md), register a second reviewer instead.

### Pin from your app

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

- **TypeScript**

  `pin-gated.ts`

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

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

  // What would the gate say? Nothing is recorded.
  const check = await kindgi.agents.pinned.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 { pin } = await kindgi.agents.pinned.pin('acme.order-desk', request);
    console.log(pin.status, pin.approvalId ?? '');
  }
  ```

- **Python**

  `pin_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"},
      "test_run_id": sys.argv[1],  # the test run 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.pinned.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":
      pin = kindgi.agents.pinned.pin("acme.order-desk", **request).pin
      print(pin.status, pin.approval_id or "")
  ```

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

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

`kindgi.releaseGates.publish`, or `release_gates.publish` in Python, and `kindgi.agents.releaseGate.resolve`, or `agents.release_gate.resolve`, do what `kindgi release-gates publish` and `kindgi agents release-gate` do.

## Next

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