Flow Actions
Actions are the operations available inside state doc flows. Each action searches, reads, or enriches items in the store.
State docs invoke actions via do: <name>. Parameters come from the rule's with: block. The runtime calls the action, receives an output dict, and makes the output available to subsequent rules via {rule_id.field} template references.
Search actions
find
Search the store by semantic query, or list items by tags/prefix.
- id: search
do: find
with:
query: "search terms"
tags: { topic: "auth" }
bias: { "now": 0, "preferred-doc": 2.0 }
limit: 10
since: "P7D"
offset: 0| Param | Type | Default | Description |
|---|---|---|---|
query | str | — | Search query text (semantic + full-text) |
similar_to | str | — | Item ID to find similar items to |
tags | dict | — | Tag filter — only items matching all specified tags |
bias | dict | — | Score multiplier per item ID: 0=exclude, 1=neutral, >1=boost |
prefix | str | — | ID prefix filter (e.g. "doc@P" for parts) |
limit | int | 10 | Maximum results |
since | str | — | ISO duration (P7D) or date — items updated since |
until | str | — | ISO duration or date — items updated before |
offset | int | 0 | Skip first N results (pagination) |
include_hidden | bool | false | Include system notes (dot-prefix IDs) |
order_by | str | updated | Sort order for list mode: updated, accessed, created |
At least one of query, similar_to, tags, prefix, or since is required. query and similar_to are mutually exclusive.
Output:
{
"results": [
{"id": "...", "summary": "...", "tags": {...}, "score": 0.87},
...
],
"count": 5,
# Statistics (added by runtime for search-mode queries):
"margin": 0.15, # score gap between #1 and #2 (0=tied, 1=dominant)
"entropy": 0.7, # score distribution spread (0=peaked, 1=uniform)
"lineage_strong": 0.0,
"dominant_lineage_tags": null,
"top_facet_tags": [{"topic": "auth"}]
}Access via {search.results}, {search.margin}, {search.entropy}, etc.
traverse
Follow edges from items to discover related items.
- id: related
do: traverse
with:
items: "{search.results}"
limit: 5| Param | Type | Default | Description |
|---|---|---|---|
items | list | required | Items to follow edges from |
limit | int | 5 | Max related items per source |
Output: {"groups": {"source-id": [{id, summary, tags}, ...], ...}, "count": N}
Context actions
get
Retrieve a specific item by ID.
- id: target
do: get
with:
id: "item-id"| Param | Type | Default | Description |
|---|---|---|---|
id | str | required | Item identifier |
Output: {"id": "...", "summary": "...", "tags": {...}} or {} if not found.
list_parts
List decomposed parts (structural children) of an item.
- id: parts
do: list_parts
with:
id: "{params.item_id}"
limit: 5| Param | Type | Default | Description |
|---|---|---|---|
id | str | required | Parent item ID |
limit | int | 5 | Maximum parts to return |
Output: {"results": [{id, summary, tags, score}, ...], "count": N}
list_versions
List version history for an item.
- id: versions
do: list_versions
with:
id: "{params.item_id}"
limit: 3| Param | Type | Default | Description |
|---|---|---|---|
id | str | required | Item identifier |
limit | int | 3 | Maximum versions to return |
Output: {"versions": [{"offset": 1, "summary": "...", "date": "2026-03-15"}, ...], "count": N}
resolve_meta
Resolve meta-document definitions against a target item. Meta-docs (.meta/*) define tag-based queries; this evaluates them against the target item's tags.
- id: meta
do: resolve_meta
with:
item_id: "{params.item_id}"
limit: 3| Param | Type | Default | Description |
|---|---|---|---|
item_id | str | required | Item to resolve meta-docs for |
limit | int | 3 | Max items per meta-doc section |
Output: {"sections": {"learnings": [{id, summary, tags}, ...], "todo": [...]}, "count": N}
resolve_edges
Resolve forward and inverse edges for an item.
- id: edges
do: resolve_edges
with:
id: "{params.item_id}"
limit: 5| Param | Type | Default | Description |
|---|---|---|---|
id | str | required | Item identifier |
limit | int | 5 | Max edges per predicate |
Output: {"edges": {"references": [{id, summary, predicate, date}, ...], ...}, "count": N}
Mutation actions
move
Move versions from a source item into a named target.
- id: moved
do: move
with:
name: "project-notes"
source: "now"
tags: { project: "myapp" }
only_current: false| Param | Type | Default | Description |
|---|---|---|---|
name | str | required | Target item ID (created if new) |
source | str | now | Source item to extract from |
tags | dict | — | Only extract versions matching these tags |
only_current | bool | false | Only move the current version, not history |
Output: {"id": "project-notes", "summary": "..."}
delete
Permanently delete an item and its version history.
- do: delete
with:
id: "old-item"| Param | Type | Default | Description |
|---|---|---|---|
id | str | required | Item to delete |
Output: {"deleted": "old-item"}
Processing actions
These run during after-write flows to enrich newly stored items.
summarize
Generate a summary for a target item using the configured LLM provider.
- do: summarize
with:
item_id: "{params.item_id}"| Param | Type | Default | Description |
|---|---|---|---|
item_id | str | required | Item to summarize |
Output: {"summary": "..."}
Mutations: [{"op": "set_summary", "summary": "..."}]
tag
Set explicit tags on one or more items. Accepts a single item ID or a list of search results.
# Single item
- do: tag
with:
id: "my-item"
tags: { project: "security-audit" }
# Bulk: tag all search results
- do: tag
with:
items: "{search.results}"
tags: { reviewed: "true" }| Param | Type | Default | Description |
|---|---|---|---|
id | str | — | Single item ID to tag |
items | list | — | List of items (result dicts or IDs) to tag |
tags | dict | required | Tags to apply |
One of id or items is required.
Output: {"count": N, "ids": ["..."], "mutations": [...]}
Mutations: [{"op": "set_tags", "target": "id", "tags": {...}}, ...]
auto_tag
Classify a target item against tag specs (.tag/* docs) in the store using an LLM. Used by the after-write flow for automatic classification.
- do: auto_tag
with:
item_id: "{params.item_id}"What gets tagged depends on which .tag/* specs exist. Users add new tagging vocabularies by creating spec docs.
Output: {"tags": {"act": "commitment", "status": "open", ...}}
Mutations: [{"op": "set_tags", "tags": {...}}]
analyze
Decompose a target item into structural parts.
- do: analyze
with:
item_id: "{params.item_id}"Output: {"parts": [{summary, content, tags, part_num}, ...]}
Mutations: [{"op": "put_item", "id": "item@p1", ...}, ...]
describe
Extract text description from images or media content.
- do: describe
with:
item_id: "{params.item_id}"resolve_duplicates
Detect items with identical content and link them via edge tags.
- id: resolve-duplicates
do: resolve_duplicates
with:
tag: duplicatesOutput: {"duplicates": ["id1", "id2", ...]} or {"skipped": true, "reason": "..."}
Mutations: [{"op": "set_tags", "tags": {"duplicates": [...]}}]
extract_links
Extract markdown links from content and create edge relationships.
- do: extract_links
with:
tag: references
create_targets: "true"generate
Raw LLM prompt — the escape hatch for custom processing.
- do: generate
with:
system: "You are a classifier."
user: "{item.content}"
max_tokens: 4096
format: json| Param | Type | Default | Description |
|---|---|---|---|
system | str | "" | System prompt |
user | str | "" | User prompt |
max_tokens | int | 4096 | Maximum tokens in response |
format | str | — | If "json", parse response as structured data |
Output: {"text": "..."} — with parsed fields merged in when format: json.
ocr
Extract text from images or scanned PDF pages.
- do: ocr
with:
item_id: "{params.item_id}"| Param | Type | Default | Description |
|---|---|---|---|
item_id | str | required | Item to OCR |
pages | list | — | Zero-indexed page numbers (PDF only) |
Output: {"text": "extracted...", "pages_processed": N}
Mutations: [{"op": "upsert_item", "content": "..."}]
resolve_stubs
Resolve auto-vivified stub items by fetching content from their URI.
- do: resolve_stubsRuns during after-write for URI-backed items that were auto-created as stubs.
Store profiling
stats
Compute store profile statistics for query planning. Returns tag distributions, date histograms, structural counts, and edge fan-in/fan-out.
- id: profile
do: stats
with:
top_k: 10| Param | Type | Default | Description |
|---|---|---|---|
top_k | int | 10 | Number of top tags to detail, and top values per tag |
Output:
{
"total": 2672,
"tags": { # Detailed stats for top_k tag keys
"project": {
"count": 248, "distinct": 9, "unique": 4,
"coverage": 0.09, "categorical": true, "edge": false,
"top": {"keep": 226, "superpowers": 6}
},
"references": {
"count": 161, "distinct": 1017, "unique": 947,
"coverage": 0.06, "categorical": false, "edge": true,
"fan_out": {"avg": 7.4, "max": 35},
"fan_in": {"avg": 1.2, "max": 17},
"top": {"REFERENCE.md": 17, "TAGGING.md": 11}
},
},
"all_tags": ["topic", "type", "project", "act", ...], # All tag key names
"dates": {
"created": {"min": "2024-05-06", "max": "2026-03-15",
"annual": {"2026": 2099}, "monthly": {"2026-03": 1796},
"daily": {"2026-03-12": 1284}},
"updated": {...},
"accessed": {...},
},
"structure": {
"sources": {"inline": 1579, "uri": 824},
"with_versions": 65,
"versions_len": {"1": 44, "2": 9, "3": 2},
"with_parts": 0,
"parts_len": {},
"edges": {
"forward": {"references": 1184, "informs": 12},
"inverse": {"referenced_by": 1184, "informed_by": 12},
"distinct_sources": 173, "distinct_targets": 1020
}
}
}Key per-tag-key fields:
- categorical: few distinct values relative to count — good for filtering
- edge: tag has
_inverse— navigable relationship - unique: values appearing exactly once — high unique means identifier-like, not useful for filtering
- fan_out/fan_in: edge cardinality both directions
Result statistics
When the find action runs in search mode, the runtime computes statistics from the result set:
| Statistic | Description |
|---|---|
margin | Score gap between #1 and #2 (0=tied, 1=clear winner) |
entropy | Score distribution spread (0=peaked at one result, 1=uniform) |
lineage_strong | Version/part lineage concentration |
dominant_lineage_tags | Tags from the dominant lineage group |
top_facet_tags | Most common non-system tag constraints |
These are available as {rule_id.margin}, {rule_id.entropy}, etc. on the rule that invoked find. List-mode queries (no search scores) produce null for score-based statistics.
Custom actions
Actions live in keep/actions/. To add a new action:
# keep/actions/my_action.py
from keep.actions import action
@action(id="my_action")
class MyAction:
def run(self, params, context):
item = context.get(params["id"])
return {"result": "..."}Actions are auto-discovered on startup. Use them in state docs with do: my_action.
See also
- FLOWS.md — How flows work
- KEEP-FLOW.md — Running flows from CLI/MCP
- FLOW_STATE_DOCS.md — Built-in state doc reference