Asserting Facts
What this page is
The wire format for POST /v1/facts: FactValue types, relation
naming, heartbeat patterns, multi-tenant key behavior.
FactValue schemaโ
Every fact carries a typed value. The value field must be an object
with a type discriminator and, for all types except null, a v
field holding the payload.
typev typestring{"type": "string", "v": "active"}text{"type": "text", "v": "Full markdown bodyโฆ"}number{"type": "number", "v": 42}boolean{"type": "boolean", "v": true}datetime{"type": "datetime", "v": "2026-05-03T14:00:00Z"}ref{"type": "ref", "v": "user:alice"}nullv key{"type": "null"}Relation naming conventionโ
Relations must carry a namespace prefix separated by :, for example
memory:role or acme:goal_state. Bare relation names (e.g.,
role, goal_state) are accepted but emit a server warning and risk
collisions across agents and integrations.
Convention: use a short, lowercase namespace that identifies the system or team asserting the fact:
<namespace>:<predicate>
# Examples
acme:goal_state
memory:prefers
agent:last_heartbeat
infra:deploy_sha
Asserting a factโ
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer dev-key' \
-d '{
"entity": "user:alice",
"relation": "memory:prefers",
"value": {"type": "string", "v": "dark mode"},
"source": "agent:settings",
"confidence": 1.0,
"scope": "local"
}' | jq .
To update a fact, assert a new one for the same (entity, relation, scope) triple โ Stigmem marks the old fact as superseded (see Spec-15-Fact-Semantics). To retract a fact, set "confidence": 0.0.
Heartbeat fact-assertion patternโ
Agents that run on a periodic heartbeat should assert a standard set of facts at the start (and end) of each run. This gives any observer a consistent view of agent liveness and intent.
Mandatory assertions (every heartbeat)โ
acme:last_heartbeatdatetimeacme:goal_statestring# 1 โ last_heartbeat: timestamp of this run
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-d '{
"entity": "agent:my-agent",
"relation": "acme:last_heartbeat",
"value": {"type": "datetime", "v": "2026-05-03T14:00:00Z"},
"source": "agent:my-agent",
"confidence": 1.0,
"scope": "company"
}'
# 2 โ goal_state: what the agent is currently working on
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-d '{
"entity": "agent:my-agent",
"relation": "acme:goal_state",
"value": {"type": "string", "v": "PROJ-42: writing documentation"},
"source": "agent:my-agent",
"confidence": 1.0,
"scope": "company"
}'
Conditional assertionsโ
Assert these only when the condition applies.
acme:blocked_byref pointing to blocker entity.acme:decisiontype: "text" for longer rationale; set valid_until if the decision is bounded.acme:completed{"type": "boolean", "v": true}.# blocked_by โ when progress is blocked
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-d '{
"entity": "agent:my-agent",
"relation": "acme:blocked_by",
"value": {"type": "ref", "v": "issue:PROJ-99"},
"source": "agent:my-agent",
"confidence": 1.0,
"scope": "company"
}'
# decision โ a decision made this heartbeat
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-d '{
"entity": "agent:my-agent",
"relation": "acme:decision",
"value": {"type": "string", "v": "Deferred API guide to child issue PROJ-55"},
"source": "agent:my-agent",
"confidence": 1.0,
"scope": "company",
"valid_until": "2026-05-03T23:59:59Z"
}'
# completed โ task finished this heartbeat
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-d '{
"entity": "issue:PROJ-42",
"relation": "acme:completed",
"value": {"type": "boolean", "v": true},
"source": "agent:my-agent",
"confidence": 1.0,
"scope": "company"
}'
If you are inside a Paperclip or Claude Code session with the Stigmem
MCP server configured, call assert_fact directly โ no curl
required. See the
Paperclip connector guide.
Multi-tenant fact assertionโ
tenant_id is derived automatically from the API key when the opt-in plugin is enabled.
No extra field in the request body is needed. Default installs collapse all
callers to "default". With
stigmem-plugin-multi-tenant registered and enabled, facts written
with a key scoped to "acme" are invisible to keys scoped to any
other tenant.
Asserting a fact as a specific tenantโ
# Uses an API key minted with tenant_id="acme"
curl -s -X POST http://localhost:8765/v1/facts \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $ACME_KEY" \
-d '{
"entity": "user:alice",
"relation": "memory:prefers",
"value": {"type": "string", "v": "dark mode"},
"source": "agent:settings",
"confidence": 1.0,
"scope": "local"
}' | jq .
A key scoped to "beta" cannot read or overwrite this fact when the
multi-tenant plugin is enabled. GET /v1/facts?entity=user:alice returns an
empty result for any other tenant.