# Settings history of a workflow (experimental)

Source: https://docs.littlebigbrain.com/guides/workflow-settings-history/

A workflow's settings change over time. Someone raises the gate, adds a
property, or moves a budget. When the results change, you want to know which
settings made them, and you want to go back to the settings that worked.

Each change of a workflow's settings writes a **revision**. A revision holds
the settings a person set: the template and its version, the trigger, the
parameters, the gate, the owned outputs, the budget and, for a workflow of
your worker, its batch and routing. Its id is a hash of those settings, such
as `wrev_5d0c9e2a41b7f3368e10c4d2a9b8f701`, so the same settings always get
the same id. The workflow runs on one revision at a time, its **active
revision**. Each run records the revision it ran on.

A revision leaves out what comes from the environment: the units of the
ontology's properties, the models, and the system prompt. The run's
fingerprint covers those. Two runs with the same revision and different
fingerprints had the same settings and another environment, for example a
new model.

## See the history

In the console, open a workflow and choose the **Settings** tab. Under the
form, **History** lists each change, newest first: when, who, and what
changed. The active revision is marked.

With the TypeScript SDK:

```ts
import { LbbClient } from "@littlebigbrain/client";

const client = new LbbClient({ baseUrl: process.env.LBB_URL!, apiKey: process.env.LBB_API_KEY, graph: "tender" });

const page = await client.workflows.named.revisions("sinks");
for (const revision of page.data) {
  console.log(revision.activation, revision.action, revision.activated_at, revision.activated_by, revision.bound);
}
```

With the Python SDK:

```python
import os

from lbb import LbbClient

client = LbbClient(base_url=os.environ["LBB_URL"], api_key=os.environ["LBB_API_KEY"], graph="tender")

page = client.workflows.named.revisions("sinks")
for revision in page.data:
    print(revision.activation, revision.action, revision.activated_at, revision.activated_by, revision.bound)
```

Each item is a revision with the facts of one activation: `activation` (its
number), `action` (`create`, `change`, `activate`, or `existing` for the
settings a workflow had before its history began), `activated_at`,
`activated_by` (the key or the account that made the change) and `bound`
(this is the active one). A revision that was active twice is listed twice.
The history keeps the newest 100 activations.

## Compare two revisions

A revision's `settings` is a small JSON object. Read two revisions and
compare their settings:

```ts
const before = await client.workflows.named.getRevision("sinks", "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c");
const after = await client.workflows.named.getRevision("sinks", "wrev_5d0c9e2a41b7f3368e10c4d2a9b8f701");
console.log(before.settings.gate, after.settings.gate);
```

The console shows what changed on each row of the history, and in the
confirmation before you go back.

## See whether a revision was tested

Before you go back to earlier settings, check whether a test ever read
inputs with them. A revision of a `values.extract` workflow carries
`last_tested`:

```ts
const revision = await client.workflows.named.getRevision("sinks", "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c");
if (revision.last_tested) {
  const { test, finished, counts } = revision.last_tested;
  console.log(test, finished, counts.inputs_done, counts.passed, counts.review, counts.failed);
} else if (revision.last_tested === null) {
  console.log("No test is recorded for these settings.");
}
```

```python
revision = client.workflows.named.get_revision("sinks", "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c")
if revision.last_tested:
    print(revision.last_tested.test, revision.last_tested.finished, revision.last_tested.counts.passed)
elif "last_tested" in revision.model_fields_set:
    print("No test is recorded for these settings.")
```

`last_tested` names the newest test that read inputs with exactly this
revision, and the counts it ended with: inputs read, values that passed the
checks, values for review, properties with no value, and failures. It shows
what happened and does not judge it. A test counts when it ended `done` and
at least one input was read by the model. An experiment's arm is a test too:
its `last_tested` has `experiment` (`id` and `arm`), and an experiment that
ended by its cost limit or the monthly budget has cancelled arms, which do
not count. Only a run of the revision as stored counts: in a model
experiment only arm A (the models in use) records a fact, and in a revision
experiment both arms record. A change of any setting, the monthly budget included, makes a new
revision with no test.

`last_tested` has three states. An object means a test read inputs with
these settings. `null` means the workflow has tests and no test is recorded
for these settings. A test that ran on an older node, or before this field
existed, is not recorded, so `null` does not prove that none ran. A response
without the key means the workflow has no tests, or the server cannot say;
that is unknown.

In the console, each row of the history shows **Tested** with the time, or
**No test recorded**. When you choose **Use these settings** on a row with no
recorded test, the confirmation warns and its button reads **Use without a
test**.
Activation is never refused for a missing test.

## Go back to earlier settings

In the console, choose **Use these settings** on a row of the history, read
what changes, and confirm.

With the SDK, activate the revision. Pass the revision you read as active,
so the call fails if someone else changed the workflow since:

```ts
const workflow = await client.workflows.named.activateRevision("sinks", "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c", {
  expectedRevision: "wrev_5d0c9e2a41b7f3368e10c4d2a9b8f701",
});
console.log(workflow.active_revision);
```

```python
workflow = client.workflows.named.activate_revision(
    "sinks",
    "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c",
    expected_revision="wrev_5d0c9e2a41b7f3368e10c4d2a9b8f701",
)
```

An activation makes the same checks as a change of the settings. It fails
with `409 workflow_revision_conflict` when another revision is active than
the one you passed, and with `409 workflow_revision_stale` when the graph
now reads those settings differently, for example after a field was
renamed. Then change the workflow with the settings you want. Activating the
active revision again changes nothing, so a retry is safe.

A change of the watched fields, the parameters or the gate makes the
workflow read its class again, as a change of the settings does. Work that
already waits or runs keeps the revision it started with; new work uses the
new one.

## Test earlier settings

A test runs a workflow on chosen inputs and writes nothing to the graph.
Pass `revision` with `name` to run it on a revision of the history instead
of the settings in use; nothing is activated. Run one test per revision on
the same inputs and compare them:

```ts
const test = await client.workflows.tests.create({
  name: "sinks",
  revision: "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c",
  sample: 6,
});
```

See [Test a workflow before it writes](https://docs.littlebigbrain.com/guides/test-a-workflow/).

## Runs name their revision

Each run summary has a `revision`. A run keeps it after the history drops
that revision; reading the revision then answers `404
workflow_revision_not_found`.

```ts
const runs = await client.workflows.named.runs("sinks");
const byRevision = Map.groupBy(runs.data, (run) => run.revision);
```

## Agents

The MCP server reads the history with `lbb_workflows` (actions `revisions`
and `revision`) and activates a revision with `lbb_workflows_manage`
(action `activate`). An activation changes what the workflow does from then
on and deletes no data, so it needs no extra approval. Show the user what
changes before you activate, and tell them when `last_tested` is `null`.

## HTTP

| Method and path | Does |
| --- | --- |
| `GET /v1/workflows/named/revisions?name=&cursor=&limit=` | The history, newest activation first |
| `GET /v1/workflows/named/revisions/get?name=&id=` | One revision |
| `POST /v1/workflows/named/revisions/activate` | `{"name", "id", "expected_revision"?}`: make a revision active |

The [errors page](https://docs.littlebigbrain.com/sdks/errors/) lists `workflow_revision_conflict`,
`workflow_revision_stale` and `workflow_revision_not_found`.
