---
name: optimize-cache
description: Build an optimized playbook + deterministic cache for a Canary workflow from a real run's Playwright trace. Materializes the run workspace, works node by node, verifies locators offline against trace-reconstructed DOMs, and commits recipes through the server-validated cache-recipe endpoint. Use when the user asks to optimize a workflow's cache, harvest a run into the cache, or repair a low-hit-rate workflow.
allowed-tools: Read, Grep, Glob, Bash(canary workflow debug:*), Bash(canary workflow cache:*), Bash(canary workflow runs:*), Bash(jq:*), Bash(diff:*), Bash(sed -n:*), Bash(head:*), Bash(tail:*), Bash(wc:*), Bash(grep:*), Bash(find:*), Bash(ls:*), Bash(cat:*)
---

<!-- AUTOGENERATED from packages/workflow-cache/. Do not edit by hand. -->
<!-- After editing the partials, mint a new version: -->
<!--   bun run --filter=@canaryai/cli bump-skill optimize-cache [patch|minor|major] -->

# Cache Optimization Guide

You are building the deterministic cache + playbook for a workflow from the
evidence of a real run — the same procedure the server's async cache-optimizer
agent follows, driven manually through the `canary` CLI.

## Materialize the workspace

```bash
canary workflow debug materialize <runId> --out ./cache-opt-<runId>/
```

The workspace contains the trace (`browser/trace.zip`, parsed
`browser/actions.jsonl`), per-step thoughts, timeline JSONLs, the workflow
definition, and variable lineage.

## CLI tools

```bash
# Reconstruct one trace action's DOM and capture its substrate-v2:
canary workflow cache substrate --workspace ./cache-opt-<runId> --action <traceActionIndex> [--phase before|after]

# Derive + offline-verify a locator for a ref in that substrate:
canary workflow cache locator --workspace ./cache-opt-<runId> --action <traceActionIndex> --ref <ref> [--var name=value ...]

# Commit a node's recipe (server recomputes the hash and validates):
canary workflow cache commit <workflowId> --node <nodeId> --recipe <recipe.json>

# Superadmin-only transfer of exact content proven by two local cache-only runs:
canary workflow cache commit <workflowId> \
  --node <nodeId> \
  --recipe <recipe.json> \
  --force \
  --expected-cache-id <current-cache-id|none> \
  --verified \
  --verified-from <local-run-1> \
  --verified-from <local-run-2> \
  --yes

# Or enqueue the server-side optimizer job instead of working manually:
canary workflow cache optimize <workflowId> [--run <runId>]
```

## Methodology — node by node, evidence first

You are building the deterministic cache and playbook for a workflow from the
evidence of a real run: its Playwright trace, the agent's tool calls and
thoughts, and the run's variable/macro records. Work methodically:

1. **Orient.** Read the workspace summary, the workflow definition
   (`workflow/`), `timeline/*.jsonl`, `variables/lineage.jsonl`, and the
   per-step thoughts (`thoughts/by-step/`). Note each step's `pathNotes`
   summary when present — the live agent recorded its detours there.
2. **Pick harvestable nodes.** Only nodes whose step SUCCEEDED are
   harvestable. A node that succeeded in a run that later failed is still
   valid — often the most valuable, since that run won't be green soon.
   Where a harvestable node's actions live depends on how it executed:
   - Agent-executed (cache miss/fallback): under its `Step N:` trace group.
   - Replayed WITH healing (`l2_gap` / `recovery`): under its
     `Cache Replay N:` group — replayed actions and the healing agent's live
     actions are recorded together there, and ALL of them are evidence of the
     proven path. Harvest these exactly like agent steps.
   `optimizer/segments.json` resolves each segment to its `nodeId`; trust it.
   A CLEANLY replayed node (its step needed no healing) carries nothing new —
   skip it.
3. **Per node, reconstruct the true path.** Cross-reference the node's trace
   actions with the agent's tool calls and thoughts. Identify what actually
   accomplished the task versus exploration.
4. **Author the most-direct playbook** of core actions (see Core actions).
   The playbook describes the shortest correct path to the node's goal — not
   a transcript of what the agent happened to do.
5. **Build and verify each locator** (see Verification bar). Materialize the
   substrate for the action's before-state, derive the locator, verify it
   offline. Iterate or degrade to a gap — never commit an unverified locator.
6. **Commit the node, then move on.** One node at a time; do not interleave.


## Core actions — what becomes a playbook step

Every CORE action on the success path becomes a playbook step (cached) or a
gap. Core actions are:

- **Mutations**: click, fill/type, select, check/uncheck, press, hover-to-open,
  navigate, file upload, drag.
- **Assertions**: explicit verify steps and inline assertions the node declares.
  Absence checks ("no error", "not present", "not shown", "hidden") are
  assertions too; do not treat them as automatic gaps just because they are
  negative. Cache them as `text_absent`, selector-backed `element_hidden`, or
  `evaluate` when the trace DOM gives a stable predicate. Cached
  `browser_assert` steps must include `cache.assertion.condition`; for
  selector-backed visibility assertions, the condition may be the human target
  label because the locator is the actual selector.
- **Captures**: values the agent saved for downstream steps (e.g.
  `browser_evaluate` with `saveAs`). These become context extractors on the
  cache row — `declaredOutputs` stay reserved for DOM-captured values.
  You may author a new capture when the run evidence shows the app generated
  a value that later cacheable work depends on, even if the live agent did not
  capture it. The capture expression must read visible DOM or the current URL,
  return a single string, and save the key used by later `$var.key` /
  `{{var.key}}` references.
- **Non-browser effects**: reading email/inbox, running python, reading or
  verifying downloads. These are invisible in the browser trace — source them
  from the agent's tool-call records and thoughts. They become playbook steps
  whose compiled entry is a **gap** (the replay agent performs them live);
  python steps with a cache label become cached data scripts.

Observation tool calls are NOT playbook material: accessibility snapshots,
screenshots, look-around scrolling, `browser_evaluate` without `saveAs`. They
informed the agent; they leave no artifact in the playbook.

A playbook that silently drops a core action loses functionality — when in
doubt whether an action was core, check what downstream steps consumed.


## Replay mechanics — author recipes that replay, not recipes that look right

The trace shows RESOLVED values; the recipe must reference their SOURCES so
future runs work with fresh data:

- **`$var.X` tokens** substitute from the variable registry at replay.
  Workflow `inputValues` auto-promote to `$var.{name}` downstream; values an
  agent captured land under the key it saved them as. Keep these tokens
  verbatim in playbook params — never inline the resolved value you saw in
  the trace.
- **Macros** (`$EMAIL()`, `$UUID()`, `$RANDOM_STRING()`, …) re-roll fresh on
  every run. The run's macro resolution records map resolved values back to
  expressions; supply action args as literals and let the synthesizer attach
  `macroBindings` — do not hand-write macro expressions into action args.
- **Dynamic locator text** (`dynamicText` / `containsTextDynamic` bindings)
  re-resolves selector text from run state. When a locator can only be
  disambiguated by a run-specific value, bind it to the variable that
  produced the value — never anchor on the literal.
- **App-generated values the live agent missed** are still recoverable here.
  If the app creates a value during this run (receipt number, confirmation
  code, generated entity ID, auto-derived title or name, URL slug) and a
  later locator, assertion, or download check depends on it, call
  `declare_dynamic_variable` with the observed value before building
  locators. Then add a cached `capture` action that saves the same key before
  the first dependent cached action, and use `$var.key` / `{{var.key}}` where
  the generated value appears later.
  Constants chosen by the workflow (names, dates, dropdown options, payment
  types) do not need this treatment unless they are app-generated.
  This is a REQUIREMENT, not a suggestion: once you suspect a distinguishing
  value is app-generated (you saw the app derive it, the workflow never typed
  it, or it differs from what an earlier step entered), you may NOT anchor on
  the literal and you may NOT silently gap the step. Find the value on-path
  (a heading, breadcrumb, row cell, or the URL of a page the step already
  visits), capture it, and bind the locator to the capture. Gap only when no
  on-path capture source exists — and say so in the gap reason.
- **Gaps** are honest: a `gap` compiled entry means "the replay agent performs
  this step live". A gap replays slower but correctly; a bad cached locator
  replays fast and wrong. Prefer the gap — except where the only obstacle is
  an app-generated value with a visible capture source (see above): there the
  capture-bound locator is the correct outcome, not a gap.
- **Volatile identifiers** (seeded entity numbers, ULIDs/UUIDs, timestamps,
  framework-generated class hashes) WILL differ next run even though they
  point at a real element today. Structural prefixes (e.g. a stable
  `__new__` draft-row prefix) are usable; the embedded number is not.


## Exploration filtering — most-direct path, not a transcript

The live agent sometimes explores: opens the wrong section, misclicks,
scrolls to look around, retries. The playbook is the shortest correct path,
so filter exploration out — but carefully:

- Use the agent's thoughts and the step's `pathNotes` ("accidentally opened X
  before finding A under B") to separate detours from the success path.
- An action is exploratory if the success path works without it. It is
  LOAD-BEARING if a later action's target only exists/becomes visible because
  of it — dismissing a blocking dialog, expanding a collapsed section,
  switching a tab. Keep those even if the agent stumbled into them.
- The offline check: if the direct-path locator for step N does not resolve
  against the post-state of step N-1, you dropped something load-bearing.
  Re-examine the actions you filtered.
- Collapse retries: a fill that was typed twice (cleared, retyped) is one
  step with the final value.


## Verification bar — never commit an unverified locator

Every cached browser action's locator must pass offline verification against
the trace-reconstructed DOM of the action's before-state:

1. Materialize the substrate for the action (its before-snapshot).
2. Pick the target `ref` from THAT substrate, not from the trace action's
   `params.selector`. Trace `aria-ref=e...` values are only hints: ref numbers
   can change when the DOM is reconstructed, so copying the trace selector can
   point at a different element. Read the substrate around the intended target
   and choose the ref in the reconstructed tree.
3. Derive the locator from the substrate (the deterministic builder owns the
   predicate ladder — do not hand-craft selectors when the builder succeeds).
4. Verify: the strategy must resolve to EXACTLY the recorded target element
   (handle identity, count of one) and must not lean on volatile identifier
   tokens.

On failure, in order:
- Re-check the target ref — did you pick the right element in the substrate,
  or accidentally reuse a trace `aria-ref` that now names something else?
- Try the builder against a corrected ref or with the run's variable values
  (a row may only be distinguishable via a bound variable).
- If the element is one of several identical widgets (responsive duplicates,
  repeated list rows with no distinguishing signal), that is IRREDUCIBLE.
  Do not reach for ordinal/nth predicates — ordinals encode layout order,
  not identity. Mark the step as a gap instead.
- Any other persistent failure: gap, with a reason.

A verified-offline locator can still fail live (reconstructed DOMs lack JS
state and cross-origin frames) — that is what replay healing is for. An
UNVERIFIED locator must never be committed.

Bound your effort per step. Make at most ONE alternate locator attempt for a
step (a corrected ref, the builder, or a single hand-written chain). If the
second offline verification still fails, STOP: mark that step a gap with a
short reason and move to the next pending step. Never re-verify the same step
again, and never chase a "functionally correct but the verifier is strict"
locator across many attempts — a gapped step replays live, and finishing the
node with finish_node always matters more than caching every step. The tool
enforces this: once you hit the cap, verify_locator returns an `escalation`
instruction to gap and move on; follow it immediately.

For selector-backed `element_hidden` assertions, verify against the
after-state where the element should be absent or hidden. There is no target
ref for absence; the verifier passes when the locator resolves to zero visible
elements in that reconstructed DOM. This is the right shape for "no error
banner/message is shown" checks. If the absence target is broad, a broad
locator such as `locator('text=/error/i')` is acceptable when it matches the
configured assertion intent and verifies hidden in the after-state.


## Commit rules — one node, one recipe

- Only nodes whose step SUCCEEDED in the source run are committable — the
  commit is rejected otherwise (enforced, not advisory). Failed or
  never-reached nodes have no proven success path to cache.
- One commit per node, after all its steps are resolved (cached or gap).
- Playbook steps are numbered contiguously from 1; every step maps to exactly
  one cached action or one gap.
- **Reconcile, don't duplicate, auto-cached fields.** Assertions the node
  declares are carried as the codified assertion on the recipe; captures are
  carried as context extractors — aligned to the new step order. Do not also
  write them as redundant steps, and never drop them.
- **Lossless tokens.** Every `$var.X` / macro the node's inputs and previous
  recipe depended on must survive into the new recipe (as a token, a variable
  binding, or a macro binding). The commit validates this and rejects recipes
  that inline run-specific values.
- **Declared dynamic variables must be captured.** If you call
  `declare_dynamic_variable` and later use that key in a locator, assertion,
  playbook param, or action arg, the committed recipe must include a cached
  `capture` action whose `args.saveAs` is the same key. Put the capture before
  the first mutation or verification that requires the value; otherwise replay
  will fail before it can hydrate the selector.
- The node hash is recomputed server-side from the node context with your
  playbook applied; the next run computes the same hash and replays your
  recipe. You never supply a hash.
- Be honest in summaries: report how many steps are cached versus gaps and
  why. A mostly-gap recipe for a hard node is a fine outcome.


## Recipe file format

`canary workflow cache commit` takes a JSON file:

```json
{
  "sourceRunId": "<runId>",
  "playbookSteps": [{ "order": 1, "action": "fill", "target": "Name field", "params": { "text": "$var.name" } }],
  "plannedActions": [{ "stepOrder": 1, "toolName": "browser_type", "actionType": "type", "args": { "text": "Acme Corp" }, "selectors": { "role": { "role": "textbox", "name": "Name" } }, "elementDescription": "Name field" }],
  "gaps": [{ "stepOrder": 2, "reason": "reads verification email from inbox" }]
}
```

`plannedActions[].args` carry the LITERAL values from the trace — the server
maps them back to variables/macros from the run's records and rejects recipes
that drop required tokens.

`--force` bypasses only the no-clobber comparison and requires the exact
incumbent id (`none` asserts no row exists), so a concurrent replacement fails
closed. It never bypasses recipe validation. Add `--verified` only when the
same recipe completed two consecutive cache-only runs against the same target
environment and credentials; pass both local run ids with repeated
`--verified-from`. The server records a `verified_import` audit transition and
routes the next ordinary run through L1. Both flags require superadmin and
`--yes`.
