Agent Memory
Apiary agents are stateless between executions by default. The only built-in context is the workflow memory document — and it dies with the workflow instance.
Agent memory adds two persistent tiers on top, written by agents themselves through an output marker and stored as plain markdown files you can read, edit, and delete by hand:
| Tier | What it holds | Lifetime | Scope |
|---|---|---|---|
| Instance (built-in) | Step data + handoff summaries | One workflow instance | The instance |
| Task | Working notes: decisions, findings, what was already tried | Until the task is terminal + retention | The task, its retries and fan-out instances, and its spawned descendants |
| Global | Durable facts: project gotchas, tooling quirks, conventions | Forever (curated) | The whole daemon — every agent, source, and workflow |
Memory is opt-in:
settings:
memory:
enabled: true
# path: /custom/root # default: <data-dir>/memory, beside apiary.db
# max_inject_chars: 4000 # prompt budget for the recall sections
# max_entry_bytes: 16384 # cap on a single memorized fact
# task_retention: 720h # prune task notes this long after the task ends
With memory disabled (the default) nothing is persisted or injected, and any
APIARY_MEMORIZE markers agents emit are stripped from output and dropped.
Writing memory — APIARY_MEMORIZE
An agent saves a memory by emitting a marker block in its step output, exactly
like APIARY_PUBLISH and APIARY_SPAWN. The block carries one JSON object or
an array of them:
APIARY_MEMORIZE_BEGIN
{
"scope": "global",
"name": "erp-precommit-stale-binary",
"description": "pre-commit lint fails when the apiary binary is stale",
"content": "When the local binary predates the config schema, pre-commit lint\nhard-errors. Rebuild the binary, or commit with --no-verify and note it in the PR."
}
APIARY_MEMORIZE_END
| Field | Required | Meaning |
|---|---|---|
scope |
no | task (default) or global |
name |
global only | kebab-case slug — the upsert key and the entry's filename |
description |
global only | one line, shown in the injected index |
content |
yes | markdown body |
- Global requests upsert
global/<name>.md: re-emitting the samenameupdates the fact (the originalcreatedtimestamp is kept). - Task requests append a timestamped, provenance-stamped note to the task's notes file. A retry re-emitting identical content is deduplicated.
- A malformed block, a rejected name, or oversized content is a warning in the daemon log — a memorize can never fail a step.
To suppress writes for a sensitive step (mirroring publish: off):
steps:
- id: untrusted-analysis
agent: analyzer
memory:
memorize: off # drop APIARY_MEMORIZE requests from this step
recall: [task] # inject task notes only, no global index
Recall — what agents see
When memory is enabled, every agent step's prompt is prefixed with two sections (ahead of the workflow memory document, on the same channel):
[Long-term Memory]
Persistent memory lives at $APIARY_MEMORY_DIR. The index is below; read the full
entry from $APIARY_MEMORY_DIR/global/<name>.md when one looks relevant. ...
- erp-precommit-stale-binary — pre-commit lint fails when the apiary binary is stale
- ci-duration — CI takes ~12 minutes on this project
[Task Memory]
- 2026-06-10T14:02:11Z [triage/analyze] (triage) Root cause is in the ADF renderer
- 2026-06-10T14:31:09Z [engineer/implement] (engineer) Chose lipgloss; tests in markdown_test.go
Key properties:
- The index is injected, not full entries. Agents read full files from
$APIARY_MEMORY_DIR(set for every step), so the prompt budget stays flat as memory grows. Both sections together are capped bysettings.memory.max_inject_chars— task notes drop oldest-first, the index truncates with an explicit(… N more entries)marker. - Task memory follows lineage. A task spawned via
APIARY_SPAWNsees its own notes first, then its parent's, then its grandparent's — so a child inherits the context its ancestors collected. memory: {read: false}on a step suppresses everything, recall included;memory.recall: [task]/[global]filters tiers.
On-disk layout
.apiary/ # the project data dir, beside apiary.yaml
memory/
MEMORY.md # global index — one line per entry, regenerated on write
global/
erp-precommit-stale-binary.md
tasks/
01JXKQ8Z3F.md # working notes for one task (append-only)
Each global entry is frontmatter + markdown body, recording provenance (agent,
task, workflow, created/updated). The files are the source of truth: edit or
delete them freely — MEMORY.md is derived and self-heals on the next write.
By default the memory root is runtime state; gitignore it like apiary.db
(**/.apiary/memory/). Because entries are plain markdown, a team can instead
point settings.memory.path at a committed directory to version and share
memory deliberately.
Lifecycle and curation
- Task notes are pruned by a daemon sweep (at startup, then every 6 hours)
once the task has been terminal longer than
task_retention— unless a spawned descendant is still running. Unknown task IDs (after a DB reset) are pruned by file age. - Global entries are never auto-pruned. Curate them via the CLI or your editor:
apiary memory path # print the memory root
apiary memory list # index view: name, description, updated, agent
apiary memory show <name> # print one entry
apiary memory rm <name> # delete one entry
apiary memory prune [--dry-run] # run the task-notes sweep on demand
Security notes
- Recalled memory is untrusted agent output. It is injected as inert
context; markers are only ever parsed out of live step output, so a memory
entry containing
APIARY_SPAWN_BEGIN…cannot trigger anything by being recalled. - Global means daemon-wide. Every agent served by the daemon can read every global entry. The store is project-scoped (it lives in the project's data dir), so different projects never share memory.
- Don't memorize secrets. Instruct agents (in soul files) to never store credentials or tokens in memory.