# Presets & Shortcuts

> Applies to: ChengOS v0.1.0+ ｜ Last verified: 2026-08-12 ｜ Source: `deploy/config/presets.yaml`, `deploy/config/shortcuts.yaml`, `crates/cheng-core/src/domain/preset.rs`, `crates/cheng-core/src/domain/shortcut.rs`, `crates/cheng-api/src/services/presets.rs`, `crates/cheng-api/src/services/shortcuts.rs`

Two YAML catalogs let you change a workflow's behaviour without editing the workflow:

| | Presets | Shortcuts |
|---|---|---|
| Changes | A **node's configuration** | The **routing** of the next turn |
| File | `presets.yaml` | `shortcuts.yaml` |
| Scope | Until changed again | One turn, or until cleared |
| Consumer | The target node | `ai/llm_branch` |

Both are merged from a global file and a per-workspace file, and both are cached with the file's
mtime as the invalidation signal — edit the YAML and the change is picked up without a restart.

```text
<global_dir>/presets.yaml                              ← shared by all workspaces
<workspace-root>/<workspace-id>/config/presets.yaml    ← per workspace, wins on id conflict
```

Per-workspace entries sharing an id with a global one **replace** it (case-insensitively); the
rest are appended in file order.

## Presets

A preset is a named, validated configuration patch for one node **type**. Selection is
two-level: preset type, then preset.

```yaml
preset_types:
  - id: tool_nodes
    label: Tool nodes
    description: Presets for tool nodes

presets:
  - id: code-safe-mode
    preset_type: tool_nodes
    node_type: tools/code
    label: Code execution safe mode
    description: Restrict file and shell behavior
    llm_visible: true
    risk_level: high
    config_patch:
      allow_write: false
      sandbox: strict
```

| Field | Meaning |
|---|---|
| `id` | Stable, file-unique identity used in commands and approval payloads |
| `preset_type` | The category id it belongs to |
| `node_type` | The node type this patch is valid for — applying it elsewhere is rejected |
| `label` | Shown in the selection list |
| `description` | Human- and LLM-facing explanation of what it changes and when to use it |
| `llm_visible` | Whether scoped LLM discovery may surface it — **default `false`** |
| `risk_level` | `low`, `medium`, or `high` |
| `config_patch` | JSON object **shallow-merged** into the target node's config |

The patch is applied through the same validation path as a normal node update, so a preset
cannot write a configuration the property panel would have rejected.

### One source of truth

`presets.yaml` is the single store on every surface. The editor's property panel "save as
preset" writes into it, and the chat preset selector, the CLI, and external channels read it
back. There is deliberately **no** second browser-local preset store to reconcile.

A user-saved preset with no category lands in `user_presets` ("Saved presets"), created on
demand, and defaults to `llm_visible: false` — a preset you saved for yourself is not
automatically offered to an agent, because that would widen what an agent can change without
review.

Because persisting one preset means rewriting the whole file, saves are serialised behind a
process-local lock; otherwise two concurrent saves would race and the second would drop the
first.

### LLM discovery is deliberately narrow

An agent does not see the workspace catalog. A preset is discoverable only when it is
`llm_visible` **and** its `node_type` is among the node types reachable as tools from the
requesting node.

And an LLM may **request** a preset switch but never apply one. The request becomes an approval
carrying the workspace, workflow, requester node, target node, preset id and label, a
`change_summary` diff, and an optional reason. It rides the normal approval infrastructure —
`~~y` / `~~n`, the CLI, web cards — under the action name `preset_switch`, so every surface can
tell it apart from a tool-execution approval. See
[Approvals & Human Review](/docs/exec-approvals-reviews).

### Failure behaviour

Listing fails **soft**: a missing or malformed `presets.yaml` logs a warning and yields an empty
catalog, so chat ingestion never crashes because of a bad workspace file.

Applying and saving fail **hard**, with typed errors: a save that did not happen must never look
like it did.

## Shortcuts

A shortcut attaches structured routing data to a turn:

```yaml
shortcuts:
  - name: Need Tools
    description: Route to the tool-capable branch
    route_mode: always
    value:
      need_tools: true

  - name: Search
    description: Route to the search action
    value:
      action:
        name: search

  - name: Deep Think
    description: Enable extended reasoning mode
    route_mode: always
    value:
      reasoning:
        mode: deep
        max_steps: 20
```

| Field | Meaning |
|---|---|
| `name` | Display name and stable identity; non-empty, unique in the file |
| `description` | One line shown in the selection list |
| `route_mode` | `once` (default — next turn only) or `always` (until cleared) |
| `value` | JSON **object** merged into the turn's routing payload |

`value` **must be an object**, because the consumer is `ai/llm_branch`, which routes on
nested-path lookups. Prefer nested maps (`action: { name: search }`) over flat dotted keys
(`action.name: search`) — the latter will not resolve.

### Why not just type it in the message

A shortcut attaches routing data **beside** the message rather than injecting JSON into the
message text. Two consequences: chat history stays clean, and the model never sees
route-control JSON as if it were natural language.

### Lifetime

The active shortcut is stored in the conversation context under `active_shortcut_v1` and
serialised into the turn's `route_shortcut` input each turn. `once` clears after one turn;
`always` persists until the user deselects it — the selector is a toggle.

This is distinct from the pending numbered selection, which is a transient snapshot of a list.

## Using them

| Surface | Presets | Shortcuts |
|---|---|---|
| Editor | Property panel selector; "save as preset" | Chat toolbar |
| Chat | Preset selector above the input | Shortcut selector above the input |
| CLI | `/presets` | `/shortcuts` |
| External channels | `~~presets`, then a number | `~~shortcuts`, then a number |

See [Channel Routing](/docs/chat-channel-routing) for the channel command set and
[Embedded Chat](/docs/chat-embedded) for the toolbar.

## Practical advice

- **Name shortcuts as intents**, not as implementation: `Deep Think` beats
  `set reasoning.mode=deep`.
- **Use `always` sparingly.** It stays on until deselected, and a forgotten sticky shortcut is
  hard to notice.
- **Keep `llm_visible: false` by default** and open it only for presets that are safe for an
  agent to request — noting the request still needs approval.
- **Set `risk_level`** on anything that widens access; it drives how the approval is presented.
- **Put shared entries in the global file** and use per-workspace files only for genuine
  overrides.

## Next steps

- [Embedded Chat in the Editor](/docs/chat-embedded) — the selectors in the UI.
- [Approvals & Human Review](/docs/exec-approvals-reviews) — the gate on LLM preset switches.
- [Environment Variable Reference](/docs/config-env-reference) — where the workspace root is set.
