Skip to content

Settings history of a workflow (experimental)

View Markdown

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.

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:

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:

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.

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

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.

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:

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.");
}
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.

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:

const workflow = await client.workflows.named.activateRevision("sinks", "wrev_e3a41f0b9c2d7e8a5b6c1d0f9e8a7b6c", {
expectedRevision: "wrev_5d0c9e2a41b7f3368e10c4d2a9b8f701",
});
console.log(workflow.active_revision);
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.

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:

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

See Test a workflow before it writes.

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.

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

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.

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 lists workflow_revision_conflict, workflow_revision_stale and workflow_revision_not_found.