Customize the extraction pipeline
memory.set_from_messages runs its transcript through an ordered list of steps that share
one state. A request can replace that list through pipeline_steps: reorder or skip steps, or
call an in-process step an operator registered. This page covers only the step list;
Extraction covers what extraction does.
pipeline_steps is an HTTP body field. It is hidden from the MCP tool schema and is not a
parameter of the Python SDK's memory.set_from_messages helper; send it over HTTP or through
call(...). The generated op input types (sdk/python/loom/ops.py,
sdk/typescript/src/generated/ops.ts) carry it.
The default pipeline
DEFAULT_PIPELINE in server/memory/app/pipeline.py:
| Step | Does |
|---|---|
loom.fetch_namespace_categories |
Loads the namespace's tag taxonomy for the extractor. |
loom.fetch_existing_memories |
Nearest existing memories (up to 50), shown to the extractor. |
loom.llm_extract |
The extraction call. |
loom.drop_recall_echoes |
Drops facts restating memories the session was served. |
loom.drop_session_echoes |
Drops facts restating what the session's own Loom calls carried. |
loom.drop_activity_narration |
Drops narration of tool activity with no durable outcome. |
loom.drop_non_durable |
Coding sessions: drops status snapshots, housekeeping, plans. |
loom.verify_durable |
Coding sessions: verifier pass over the remaining facts. |
loom.hash_dedup |
Drops facts whose canonical hash is already stored. |
loom.bulk_write |
Files bot rules (about="agent") to the learnings queue, writes the rest via memory.bulk_set. |
A coding session is a transcript whose extra_tags carry a coding-agent source tag
(source:claude-code, source:codex, source:cursor), or a trajectory replay. The two coding
steps pass other transcripts through unchanged.
An empty pipeline_steps runs this list. A non-empty one runs exactly the steps it
names, in order, against one shared state. Leaving out loom.bulk_write means nothing is
written: the response still reports what was extracted.
Run a shorter pipeline
Extract and write with no dedup context, for a one-shot import of data known to be new:
curl -s "$LOOM_BASE_URL/v1/memory.set_from_messages" \
-H "Authorization: Bearer $LOOM_API_KEY" -H 'Content-Type: application/json' -d '{
"org": "acme",
"namespace": "support",
"agent": "importer",
"messages": [{ "role": "user", "content": "..." }],
"pipeline_steps": [{ "step_id": "loom.llm_extract" }, { "step_id": "loom.bulk_write" }]
}'
This needs a real LLM provider (LOOM_LLM_PROVIDER and its key); the stock stack's fake
provider does not extract meaningful facts.
Step behaviour
Each descriptor is {"step_id": ..., ...}; the whole object is passed to the step as its
config.
- At most 16 steps per request.
hook.*steps are refused with 422VALIDATION_FAILED: the server makes no outbound call on a caller's behalf.- Unknown step ids are skipped and reported in the response's
pipeline_errorsasunknown_step:<id>. - Fail-open steps. A fetch step that fails records the error in
pipeline_errors(fetch_existing_failed:<error>, say) and the pipeline continues. loom.llm_extractfails closed: an extraction error aborts the request with 502, so a transcript is never silently answered withwritten=[]. It accepts{"judge": true}in its descriptor to turn the judge gate on;judge_extraction=trueon the request does the same for the default list.- Timing. The response's
stage_mscarries each step's wall time (step.<step_id>). Each step also runs inside an OpenTelemetry span namedpipeline.stepwith the step id as an attribute.
Register an in-process step
An operator registers a step in code that the server process imports at start-up:
from server.memory.app.pipeline import WorkflowState, register_step
async def spam_filter(state: WorkflowState, config: dict) -> WorkflowState:
state.extracted_facts = [
f for f in state.extracted_facts if "spam" not in str(f.get("value", "")).lower()
]
return state
register_step("operator.spam_filter", spam_filter)
Requests can then name {"step_id": "operator.spam_filter"}; any other keys in the
descriptor arrive as config. The step runs in-process with read and write access to the
whole WorkflowState: identity, messages, existing memories, extracted facts, counters.
register_step replaces an existing registration under the same id, so an operator step
can also replace a built-in.
A step that replaces loom.bulk_write must leave about="agent" facts out of what it
writes: those are the bot's rules, which the built-in files to the learnings queue.
Warning: a step that drops or rewrites
extracted_factschanges what every caller naming it stores. A step placed afterloom.bulk_writesees the written rows instate.writtenbut cannot un-write them.
To add a built-in step to Loom itself, add the function and its register_step call in
server/memory/app/pipeline.py, place it in DEFAULT_PIPELINE, and run
make bench-quick before and after: the default list shapes every extraction.
Limits
- Steps run sequentially; there is no branching or parallelism.
- The default list is global; there is no per-namespace pipeline.
- No step-level retry.