keep flow
Most flows in keep run behind the scenes — put triggers background
processing, get assembles context, find resolves queries. You never
see them. But sometimes you want to run a flow directly: re-process a
note, run a custom workflow, or pick up where an ambiguous query left off.
keep flow is the command for that. It runs any state doc — built-in or
custom — and gives you the result. If the flow stops partway (budget
exhausted, ambiguous results), you get a cursor to resume it. You can
adjust parameters between calls to steer the flow in a different direction.
For how flows work internally, see FLOWS.md. For built-in state doc reference, see FLOW_STATE_DOCS.md.
Running built-in state docs
Every flow is defined by a state doc stored as .state/<name>. Run one
by name:
# Re-process a note (summarize, tag, analyze)
keep flow after-write --target %abc123
# Assemble display context
keep flow get-context --target myproject
# Deep search with tag-edge traversal
keep flow find-deep -p query="OAuth2 design" -p limit=10
# Multi-step query resolution with custom thresholds
keep flow query-resolve -p query="auth patterns" -p margin_high=0.15The --target flag sets the target note (params.id), making it
available to all actions in the flow. The -p flag sets arbitrary
parameters — thresholds, queries, limits — that the state doc's rules
reference.
Running custom state docs
You can write your own state docs and run them from files. This is how you build custom workflows: bulk operations, periodic reviews, data pipelines — anything that combines keep's actions (find, get, tag, summarize, analyze, traverse) into a sequence with conditions.
# review.yaml — find all draft notes
match: sequence
rules:
- id: drafts
do: find
with:
query: ""
tags: { status: draft }
limit: 20
- when: "drafts.count == 0"
return:
status: done
with: { message: "No drafts found" }
- return:
status: done
with: { drafts: "$drafts.results" }keep flow --file review.yamlPiping from stdin
Agents can generate state docs on the fly and pipe them in:
cat <<'YAML' | keep flow --file - --target myproject
match: all
rules:
- id: similar
do: find
with:
similar_to: "{params.id}"
limit: 5
- id: tags
do: get
with:
id: "{params.id}"
YAMLThe --file - flag reads YAML from stdin. When combined with
--target, the piped state doc can reference params.id to operate
on the specified note.
Resuming and steering
Flows don't always finish in one call. A multi-step query might exhaust its budget. A branching search might stop with ambiguous results that need human or agent judgment. When this happens, the response includes a cursor — a token encoding where the flow stopped and what it found so far.
The key insight: the cursor carries the flow's state, but **parameters are always fresh**. You provide them on each call. This is how you steer — review partial results, then adjust the query, thresholds, or strategy before resuming.
# Start a query resolution flow
RESULT=$(keep flow query-resolve -p query="auth" --budget 3 --json)
# Flow stopped — check what it found
echo "$RESULT" | jq .status
# → "stopped"
echo "$RESULT" | jq '.bindings.search.results[:3]'
# Results are too scattered — tighten the margin and resume
CURSOR=$(echo "$RESULT" | jq -r .cursor)
keep flow --cursor "$CURSOR" -p margin_high=0.10 --budget 5
# Or refine the query entirely
keep flow --cursor "$CURSOR" -p query="OAuth2 token refresh" --budget 5Each resume gets a fresh budget. The cursor's tick count is historical
(for diagnostics), not a remaining balance — --budget 5 always means
5 new ticks.
What's in a cursor
Cursors are opaque tokens — pass them to --cursor to resume, but
don't parse or construct them. The format may change between versions.
They are self-contained (no database, no server state), so they work across CLI invocations, MCP calls, and piped workflows. You can store them, pass them between processes, or discard them.
Three things are deliberately not in the cursor — they come from the caller each time:
- Params — the caller's intent (query, thresholds, target note)
- Budget — resource allocation for this invocation
- State doc source — which flow definition to use
This separation means the flow remembers what it found, but the caller always controls what to search for and how far to go.
Implementation detail: cursors are currently base64url-encoded JSON
containing the state name, tick count, and accumulated bindings.
Composing flows
Flows return structured JSON, so they compose with other commands:
# Re-process all project notes
for id in $(keep list --prefix project/ --json | jq -r '.[].id'); do
keep flow after-write --target "$id"
done
# Use flow output to drive further actions
RESULT=$(keep flow query-resolve -p query="deployment checklist" --json)
echo "$RESULT" | jq -r '.bindings.search.results[].id' | while read id; do
keep get "$id"
doneError handling
When a flow errors, the response has "status": "error" and the
data field contains details. Errored flows have no cursor — they
cannot be resumed.
RESULT=$(keep flow nonexistent-state --json 2>/dev/null)
echo "$RESULT" | jq .status
# → "error"Command reference
CLI: keep flow
keep flow <state> [options] # run a stored state doc
keep flow --file <path> [options] # run from a YAML file
keep flow --file - [options] # run from stdin
keep flow --cursor <token> [options] # resume a stopped flow| Flag | Short | Description |
|---|---|---|
--target ID | -t | Target note ID (sets params.id) |
--budget N | -b | Max ticks this invocation (default: from config) |
--cursor TOKEN | -c | Resume a stopped flow from its cursor |
--file PATH | -f | Load state doc from file, or - for stdin |
--param key=value | -p | Set a flow parameter (repeatable) |
Output is a JSON object:
| Field | Present | Description |
|---|---|---|
status | Always | done, error, or stopped |
ticks | Always | Number of ticks consumed |
data | When flow returns data | Payload from return.with |
bindings | When actions produced output | Accumulated results keyed by action ID |
history | When multiple states visited | State names in order |
cursor | When stopped | Opaque token to resume the flow |
MCP: keep_flow
keep_flow(
state="query-resolve",
params={"query": "auth patterns", "limit": 10},
budget=5,
cursor=None, # or a cursor token from a previous call
state_doc_yaml=None, # or inline YAML string
)Same JSON structure as the CLI. The state_doc_yaml parameter is the
MCP equivalent of --file — pass inline YAML instead of a state name.
See also
- FLOWS.md — How flows work, state transitions, background processing
- FLOW_STATE_DOCS.md — Built-in state doc reference
- design/STATE-DOC-SCHEMA.md — State doc YAML schema
- design/STATE-ACTIONS.md — Available actions