Tags are key-value pairs attached to every item. They enable filtering, organization, and commitment tracking.
Multiple values per key are allowed. Setting an additional value for the same key adds it (deduplicated), rather than overwriting. Each key supports up to 512 distinct values.
Some tags are singular — new values replace old ones instead of accumulating. The act and status tags are singular: keep tag ID --tag status=fulfilled on a doc with status=open results in only fulfilled, not both.
keep put "note" -t project=myapp -t topic=auth # On create
keep now "working on auth" -t project=myapp # On now update
keep tag ID --tag key=value # Add/update tag on existing item
keep tag ID --remove key # Remove a tag
keep tag ID1 ID2 --tag status=done # Tag multiple itemsWhen indexing documents, tags are merged in this order (later wins):
- Existing tags — preserved from previous version
- Config tags — from
[tags]section inkeep.toml - Environment tags — from
KEEP_TAG_*variables - User tags — passed via
-ton the command line
Set tags via environment variables with the KEEP_TAG_ prefix:
export KEEP_TAG_PROJECT=myapp
export KEEP_TAG_OWNER=alice
keep put "deployment note" # auto-tagged with project=myapp, owner=aliceAdd a [tags] section to keep.toml:
[tags]
project = "my-project"
owner = "alice"
required = ["user"] # Enforce required tags on put()
namespace_keys = ["category", "user"] # LangGraph namespace mappingThe required list enforces that specified tag keys must be present on every put() call. The namespace_keys list configures how LangGraph namespace components map to tag names — see LANGCHAIN-INTEGRATION.md.
The -t flag filters results on find, list, get, and now:
keep find "auth" -t project=myapp # Semantic search + tag filter
keep find "auth" -t project -t topic=auth # Multiple tags (AND logic)
keep list --tag project=myapp # List items with tag
keep list --tag project # Any item with 'project' tag
keep get ID -t project=myapp # Error if item doesn't match
keep now -t project=myapp # Find now version with tagThe Python API exposes list_tags() directly:
kp.list_tags() # All distinct tag keys
kp.list_tags("project") # All values for the 'project' tagFrom the CLI, list notes that have a tag key set:
keep list -t project # Any note with a 'project' tag
keep list -t project=myapp # Notes with project=myappTwo tags help organize work across boundaries:
| Tag | Scope | Examples |
|---|---|---|
project |
Bounded work context | myapp, api-v2, migration |
topic |
Cross-project subject area | auth, testing, performance |
# Project-specific knowledge
keep put "OAuth2 with PKCE chosen" -t project=myapp -t topic=auth
# Cross-project knowledge (topic only)
keep put "Token refresh needs clock sync" -t topic=auth
# Search within a project
keep find "authentication" -t project=myapp
# Search across projects by topic
keep find "authentication" -t topic=authFor more on these conventions: keep get .tag/project and keep get .tag/topic.
For domain-specific organization patterns: keep get .domains.
Tags on find are pre-filters on the vector search, not post-filters. When you search with -t user=alice, the similarity search only considers notes tagged user=alice — you get the best matches within that scope, not global results filtered afterward. This makes tags suitable for data isolation.
Pattern: scoped search with required_tags
# keep.toml
[tags]
required = ["user"]With this config, every put() must include a user tag (or it raises ValueError). Pair it with tag filters on every search to get per-user isolation:
kp.put("my note", tags={"user": "alice"}) # enforced by required_tags
kp.find("auth", tags={"user": "alice"}) # only searches alice's noteskeep put "my note" -t user=alice
keep find "auth" -t user=alice # scoped to aliceNote: required_tags enforces tags on writes only. The caller is responsible for passing the same tag filter on reads. Without the filter, find searches across all notes.
This pattern works for any isolation key — user, project, tenant, session, etc. The LangChain integration automates this: namespace components become tags on both writes and searches.
Two tags — act and status — make the commitment structure of work visible. These are constrained tags: only pre-defined values are accepted.
# Track a commitment
keep put "I'll fix the auth bug" -t act=commitment -t status=open -t project=myapp
# Query open commitments and requests
keep list -t act=commitment -t status=open
keep list -t act=request -t status=open
# Mark fulfilled
keep tag ID --tag status=fulfilled
# Record an assertion or assessment (no lifecycle)
keep put "The tests pass" -t act=assertion
keep put "This approach is risky" -t act=assessment -t topic=architectureFor inline reference: keep get .tag/act and keep get .tag/status.
Every tag key can have a description document at .tag/KEY. These are installed automatically on first use and serve as living documentation:
keep get .tag/act # Speech-act categories
keep get .tag/status # Lifecycle status values
keep get .tag/type # Content type values
keep get .tag/project # Project tag conventions
keep get .tag/topic # Topic tag conventionsTag descriptions have their own summary and embedding, so they participate in semantic search. They also contain structured information that the analyzer uses when auto-tagging parts during keep analyze.
Some tags are constrained — only pre-defined values are accepted. When a .tag/KEY document has _constrained: true in its tags, keep validates that every value you assign has a corresponding sub-document at .tag/KEY/VALUE.
keep put "note" -t act=commitment # ✓ .tag/act/commitment exists
keep put "note" -t act=blurb # ✗ ValueError: no .tag/act/blurbThe error message lists valid values:
Invalid value for constrained tag 'act': 'blurb'. Valid values: assertion, assessment, commitment, declaration, offer, request
You can extend constrained tags by creating new sub-documents:
keep put "Active work in progress." --id .tag/status/working
# Now status=working is acceptedSome tags are open-ended but still validated against a pattern. When a
.tag/KEY document has _value_regex: '...', keep validates each assigned
value against that regular expression.
Unlike _constrained: true, pattern-constrained tags do not require child
docs at .tag/KEY/VALUE. _constrained and _value_regex are mutually
exclusive on one tagdoc.
For edge tags, regex validation applies to the canonical target note ID, not the literal surface form of a labeled ref.
keep put "Investigate restart behavior" --id restart-debug -t frame='debugging?'
keep put "Investigate restart behavior" --id restart-debug -t frame='debugging'
# ✗ ValueError: Invalid value for tag 'frame': 'debugging'. Value must match regex '^.+\?$'The bundled frame tag uses this to require note IDs ending in ?, so
frame: debugging? is valid but frame: debugging is not.
Some tags are singular — at most one value is allowed per key. When a .tag/KEY document has _singular: true in its tags, new values replace old ones instead of accumulating via set-union.
keep put "fix auth" -t status=open -t act=commitment
keep tag ID --tag status=fulfilled # replaces open → fulfilled
keep get ID # status: fulfilled (not [open, fulfilled])Providing multiple values for a singular key in one call is an error:
keep tag ID --tag status=open,fulfilled # ✗ ValueError: singular tagA tag can be both _constrained and _singular. The act and status tags are both — values are validated against sub-documents and only one value is kept.
To make a custom tag singular, set _singular: true on its tagdoc:
keep put "$(cat <<'EOF'
---
tags:
_singular: "true"
---
# Tag: `priority`
Priority level. Only one value at a time.
EOF
)" --id .tag/prioritykeep ships with these tag descriptions:
| Tag | Constrained | Singular | Values | Purpose |
|---|---|---|---|---|
act |
Yes | Yes | commitment, request, offer, assertion, assessment, declaration |
Speech-act category (what the speaker is doing) |
status |
Yes | Yes | open, blocked, fulfilled, declined, withdrawn, renegotiated |
Lifecycle state of commitments/requests/offers |
type |
No | No | conversation, paper, vulnerability, file, person, project |
Entity type (graph node label) |
kind |
No | No | learning, breakdown, gotcha, reference, teaching, meeting, pattern, possibility, decision |
Content classification |
project |
No | No | (user-defined) | Bounded work context |
topic |
No | No | (user-defined) | Cross-cutting subject area |
Constrained tags (act, status) also have individual sub-documents (e.g., .tag/act/commitment, .tag/status/open) that describe each value in detail.
Unconstrained tags (type, project, topic) accept any value. Their descriptions document conventions but don't enforce them.
The bundled frame tag is pattern-constrained rather than enumerated: it is an
edge tag whose target note ID must end in ?.
Some tags also define edges — navigable relationships between documents. See EDGE-TAGS.md for details.
Tag descriptions feed into analysis through two independent paths:
When you pass -t to keep analyze, the full content of each .tag/KEY document is prepended to the analysis prompt as context. This guides the LLM's decomposition — how it splits content into parts and what boundaries it recognizes.
keep analyze doc:1 -t topic -t projectThis fetches .tag/topic and .tag/project descriptions and includes them in the analysis prompt, producing better part boundaries and more consistent tagging. Any tag doc participates in guide context, whether constrained or not.
After decomposition, a second LLM pass classifies each part. The TagClassifier loads all constrained tag descriptions (those with _constrained: true) and assembles a classification prompt from their ## Prompt sections:
- The parent doc's
## Promptsection (e.g., from.tag/act) provides overall guidance for the tag key - Each value sub-doc's
## Promptsection (e.g., from.tag/act/commitment) describes when to assign that specific value
The classifier assigns tags only when confidence exceeds the threshold (default 0.7). Tags without a ## Prompt section use their full content as a fallback description.
To customize classification behavior, edit the ## Prompt section in a tag doc — the classifier only sees ## Prompt content, not the surrounding documentation.
Tags prefixed with _ are protected and auto-managed. Users cannot set them directly.
Implemented: _created, _updated, _updated_date, _accessed, _accessed_date, _content_type, _source
See SYSTEM-TAGS.md for complete reference.
kp.tag("doc:1", {"status": "reviewed"}) # Add/update tag
kp.tag("doc:1", {"obsolete": ""}) # Delete tag (empty string)
kp.list_items(tags={"project": "myapp"}) # Exact key=value match
kp.list_items(tag_keys=["project"]) # Any doc with 'project' tag
kp.list_tags() # All distinct tag keys
kp.list_tags("project") # All values for 'project'See PYTHON-API.md for complete Python API reference.
- META-TAGS.md — Contextual queries (
.meta/*) - PROMPTS.md — Prompts for summarization, analysis, and agent workflows
- SYSTEM-TAGS.md — Auto-managed system tags
- KEEP-LIST.md — List and filter by tags
- KEEP-FIND.md — Search with tag filters
- LANGCHAIN-INTEGRATION.md — LangChain/LangGraph namespace-to-tag mapping
- REFERENCE.md — Quick reference index