Tutorial
Real-LLM skill maintenance
Most teams keep an agent's instructions in a prompt file. When real work teaches the agent a lesson — a rule that misfired, a shortcut that worked — someone edits the file by hand, and the incident that motivated the edit is lost. The tempting shortcut is to let the model rewrite its own instructions, but then one persuasive message can change production behavior.
Memseek treats this as a data problem. The skill lives as ordinary versioned records. What happened at work is recorded as ordinary evidence records. A bounded pipeline lets a model draft a revised skill that must cite that evidence, and a person — or your application's own evaluation harness — explicitly promotes the draft to live. Nothing the model writes becomes active on its own.
In this tutorial you will run that cycle once, end to end, with a real model:
- Install a refund-reply skill: a real support procedure stored as three keyed records.
- Record what a week of replies taught: several that worked, and one where a blanket discount-code rule turned a billing-error refund into an escalation.
- Let the shipped
skillpipeline prepare a cited candidate revision. - Inspect exactly what would change — one step — and promote it.
- Render the updated, prompt-ready skill the agent uses from now on.
Prefer to watch it run first?
examples/skill_maintenance.pyruns this cycle end to end in your terminal — animated, with the interactive promote gate — against a live stack. Run it any time withuv run python examples/skill_maintenance.py.The script goes one step further than this page. Instead of writing the evidence by hand, it binds an artifact use for the prompt the assistant ran on and reports four outcomes against the returned ID, so the evidence arrives the way it does in production. Read this page first for the mechanics of the cycle, then "Where the evidence comes from in production" for the part the script adds.
The response excerpts below are shortened to the fields that matter. Real record IDs are full UUIDs; the repeated-digit IDs in the examples exist only to make the data flow easy to follow.
If you are new to Memseek¶
Six ideas cover everything this tutorial touches:
- Record — one immutable row: an entity name, a collection, a type, text, and optional structured content. Records are never edited in place; newer versions supersede older ones.
- Entity — the subject a record is about. This tutorial uses exactly one
entity,
skill:refund-replies. - Collection — a schema-checked bucket for records. The
skillscollection is keyed: each(entity, key)pair has one current value and a full version history. Theoutcomescollection is event-shaped: an append-only log of things that happened. - Worker and readiness — enrichment (embeddings, importance scores) runs
in a background worker. A record becomes search- and trigger-visible when
it reports
ready: true. - Pipeline and Promotion — a Pipeline is a bounded, YAML-declared
computation (this one is
derivations/skill.yamlin the repository). A Pipeline that declaresreview: requiredcan only stage draft records; Promotion is the explicit, atomic step that activates them. - Artifact — a deterministic template that turns current records into a bounded, prompt-ready string. No model call is involved in rendering.
That is enough to follow along. For the deeper treatment, see Core concepts.
The starting skill¶
Imagine a customer-support assistant at a small SaaS company. When a refund request lands in the queue, the assistant drafts the reply a human agent reviews and sends. Its operating instructions are a skill with three keyed sections. This is the complete starting content — read it the way the assistant does.
steps — the reply procedure:
1. Greet the customer by name and thank them for reaching out.
2. Restate the specific order or charge they mentioned, so it's clear you
understood.
3. Give the refund decision in the first two sentences — don't bury it.
4. If approved, state the amount and that it arrives in 5-10 business days.
5. Always include a discount code for their next order before closing.
pitfalls — rules learned from past mistakes:
- Don't ask for information the customer already gave you in their first
message.
- Don't promise a refund before it's actually approved.
- Don't close the reply without a clear next step or timeline.
examples — worked cases:
"I was charged twice for order #A-2231": confirm the duplicate charge, approve
the refund, state the amount and the 5-10 day timeline, and apologize for the
hassle.
"I want to cancel and get this month back": confirm the plan, explain what is
and isn't refundable, and give the exact amount being returned.
Every line here is defensible. Step 5 — always attach a discount code — was added after a churn scare, and for months it did its job: refund replies felt generous, and next-order conversion ticked up.
The reply that backfired¶
One week in, a request arrives that the playbook handles wrong. A customer, Dana, was charged twice for the same order — a bug on our side submitted the charge twice. The assistant drafts this:
Hi Dana — thanks for flagging this. You were charged twice for order
#A-2231; I've approved a full refund of $79.00, and it will arrive in
5-10 business days.
As a thank-you, here's 10% off your next order: WELCOME10.
Steps 1–4 are exactly right: it greets Dana, restates the duplicate charge, gives the decision up front, and states the amount and timeline. Then step 5 fires, and the reply attaches a coupon. Dana writes back:
Isolating what didn't work¶
Before touching anything, be precise about which part of the skill failed.
Most of steps worked. Greeting, restating the charge, leading with the
decision, and stating the timeline were exactly what the review later
endorsed. The same procedure had drafted dozens of clean refund replies that
week without complaint.
examples was silent, not wrong. Its billing-error case even models the
right instinct — it apologizes and never mentions a coupon — but a worked
example is illustrative, not binding. Step 5's blanket "always" overrode it.
pitfalls was not implicated. The reply violated none of them; the
failure came from a step doing precisely what it said.
One steps line caused the escalation:
- Always include a discount code for their next order before closing.
The rule hardcodes a discount onto every reply, so a genuine goodwill gesture becomes a tone-deaf upsell the moment the charge was our own mistake. Offering Dana a coupon right after wrongly taking her money read as us profiting from the error, and it turned a clean refund into a manager escalation.
The review made a specific decision about that line:
Attach a discount code only for goodwill or retention cases. When the charge was our own error, apologize and fix it — do not offer a coupon.
That decision is exactly the kind of lesson that usually dies in a thread. In the rest of this tutorial it becomes evidence, and the skill revises itself — under review.
The maintenance cycle at a glance¶
the agent works, using the ACTIVE skill
│
v
(1) outcomes, exceptions, and feedback are written
as ordinary event records
│
│ skill.default trigger fires
│ (10-minute cooldown)
v
(2) the skill derivation drafts a complete proposal
- every section must cite visible record ids
- stored as DRAFT records; the live skill is untouched
│
v
(3) a person or application reviews what would change,
runs evaluations, and decides
│ POST /promote
v
(4) all sections go live together as new versions;
the drafts and full history remain for audit
│
└── the agent's next render picks it up
Only steps 1 and 4 change what the agent sees. Step 2 is model work; step 3 is the deliberate pause that keeps one persuasive message from becoming production behavior.
Start Memseek with a real model¶
The setup matches Getting started except for two
things: LLM_FAKE=0 and real provider credentials. Create a disposable
local database and configure the OpenAI-compatible provider you use:
export DATABASE_URL=postgresql://postgres:postgres@127.0.0.1:55432/memseek_test
export LLM_FAKE=0
export OPENAI_API_KEY='replace-me' # the variable conf/models.yaml names in api_key_env
make database
uv run memseek migrate
In conf/models.yaml, point the shipped aliases at models your account can
use. Each alias has one job in this tutorial: the maintenance pass uses
cheap to reason about a small patch and strong to write the complete
candidate; skill and evidence records need embed for search readiness; and
outcome records use importance_scorer during enrichment.
providers:
openai:
adapter: openai_compat
base_url: https://api.openai.com/v1
api_key_env: OPENAI_API_KEY
aliases:
cheap:
targets: ["openai:gpt-4o-mini"]
params: {temperature: 0, max_output_tokens: 1200}
strong:
targets: ["openai:gpt-4o"]
params: {temperature: 0, max_output_tokens: 4000}
importance_scorer:
targets: ["openai:gpt-4o-mini"]
params: {temperature: 0, max_output_tokens: 800}
embedding:
provider: openai
model: text-embedding-3-small
dimensions: 1536
space: default-v1
Run the API and worker in separate terminals. They need the same provider settings because the worker enriches records and performs the maintenance pass.
# Terminal A — the same DATABASE_URL, LLM_FAKE, and provider exports as above
uv run uvicorn memseek.api:app --host 127.0.0.1 --port 8000
Create a workspace for the tutorial and keep its bearer key in your shell:
workspace_json="$(uv run memseek create-workspace refund-replies-demo)"
export MEMSEEK_API_KEY="$(printf '%s' "$workspace_json" | \
uv run python -c 'import json,sys; print(json.load(sys.stdin)["api_key"])')"
export MEMSEEK_AUTH="Authorization: Bearer $MEMSEEK_API_KEY"
Step 1 — install the skill¶
A skill in Memseek is not a prompt string. It is a set of keyed records in
the skills collection, one record per section. The three sections shown
above become three records that share the same shape:
| Field | Value |
|---|---|
entity |
skill:refund-replies — groups the sections under one subject |
collection / type |
skills / skill |
key |
steps, pitfalls, or examples — names the section |
text |
the section content shown above, newlines and all |
dedupe_key |
e.g. refund-replies:skill:v1:steps — makes the write safe to retry; resending the same payload reports a duplicate instead of inserting twice |
Reproduce the write (the text values are the sections above with \n
escapes):
curl -sS -X POST http://127.0.0.1:8000/records \
-H "$MEMSEEK_AUTH" -H 'Content-Type: application/json' \
-d '{"records":[
{"entity":"skill:refund-replies","collection":"skills","type":"skill","key":"steps",
"text":"1. Greet the customer by name and thank them for reaching out.\n2. Restate the specific order or charge they mentioned, so it'\''s clear you understood.\n3. Give the refund decision in the first two sentences — don'\''t bury it.\n4. If approved, state the amount and that it arrives in 5-10 business days.\n5. Always include a discount code for their next order before closing.",
"dedupe_key":"refund-replies:skill:v1:steps"},
{"entity":"skill:refund-replies","collection":"skills","type":"skill","key":"pitfalls",
"text":"- Don'\''t ask for information the customer already gave you in their first message.\n- Don'\''t promise a refund before it'\''s actually approved.\n- Don'\''t close the reply without a clear next step or timeline.",
"dedupe_key":"refund-replies:skill:v1:pitfalls"},
{"entity":"skill:refund-replies","collection":"skills","type":"skill","key":"examples",
"text":"\"I was charged twice for order #A-2231\": confirm the duplicate charge, approve the refund, state the amount and the 5-10 day timeline, and apologize for the hassle.\n\"I want to cancel and get this month back\": confirm the plan, explain what is and isn'\''t refundable, and give the exact amount being returned.",
"dedupe_key":"refund-replies:skill:v1:examples"}
]}' | uv run python -m json.tool
Direct API writes like this one become active immediately — the review
requirement applies to what the model proposes later, not to you. The
worker adds embeddings in the background; each record reports ready: true
once enrichment commits.
Read the current skill back at any time:
curl -sS \
'http://127.0.0.1:8000/document?entity=skill%3Arefund-replies&collections=skills' \
-H "$MEMSEEK_AUTH" | uv run python -m json.tool
The response carries one belief per key. The steps and examples beliefs
have the same shape as this pitfalls excerpt:
{
"entity": "skill:refund-replies",
"status": "active",
"beliefs": [
{
"key": "pitfalls",
"text": "- Don't ask for information the customer already gave you in their first message.\n- Don't promise a refund before it's actually approved.\n- Don't close the reply without a clear next step or timeline.",
"status": "active",
"ready": true
}
]
}
The agent never queries these rows directly. Its integration renders the
maintained_skill artifact, which assembles the current active sections
into one bounded, fenced block — you will see that render at the end, after
the skill has changed.
Step 2 — record what actually happened¶
Now turn the analysis into evidence. Each finding from
"Isolating what didn't work" becomes one
record in the event-shaped outcomes collection, under the same entity,
with a type that says what it is:
| Type | Evidence text |
|---|---|
outcome |
Replies that state the refund decision in the first two sentences and give the 5-10 day timeline get quick, friendly acknowledgements and close on the first reply. |
exception |
A customer who was double-charged by our own billing system got a reply that approved the refund and then offered a 10% discount code. They replied that being pitched a discount right after we took their money by mistake felt tone-deaf, and asked to speak to a manager. |
feedback |
A support lead's guidance: when the charge was our own mistake, apologize and fix it — don't attach a discount offer. Save discount codes for goodwill and retention, not for cases where we are at fault. |
The outcome protects the working steps from an overeager rewrite; the
exception indicts one step line; the feedback records the approved
replacement. Notice what these records are not: none of them tells the model
how to rewrite the skill. They are facts about the work.
Reproduce the writes:
curl -sS -X POST http://127.0.0.1:8000/records \
-H "$MEMSEEK_AUTH" -H 'Content-Type: application/json' \
-d '{"records":[
{"entity":"skill:refund-replies","collection":"outcomes","type":"outcome",
"text":"Replies that state the refund decision in the first two sentences and give the 5-10 day timeline get quick, friendly acknowledgements and close on the first reply.",
"content":{"kind":"success"},"dedupe_key":"refund-replies:evidence:001"},
{"entity":"skill:refund-replies","collection":"outcomes","type":"exception",
"text":"A customer who was double-charged by our own billing system got a reply that approved the refund and then offered a 10% discount code. They replied that being pitched a discount right after we took their money by mistake felt tone-deaf, and asked to speak to a manager.",
"content":{"kind":"failure"},"dedupe_key":"refund-replies:evidence:002"},
{"entity":"skill:refund-replies","collection":"outcomes","type":"feedback",
"text":"A support lead'\''s guidance: when the charge was our own mistake, apologize and fix it — don'\''t attach a discount offer. Save discount codes for goodwill and retention, not for cases where we are at fault.",
"content":{"kind":"review"},"dedupe_key":"refund-replies:evidence:003"}
]}' | uv run python -m json.tool
In production this is the only step your application repeats. Every shift, every retro, every piece of user feedback becomes another evidence record, and the rest of the cycle follows from it.
Where does this evidence come from at scale? Writing it by hand is fine for a retro, but the outcomes worth learning from usually surface at the point of use — a thumbs-down on a reply, a correction from a support lead, an evaluator score. Artifact uses automate the connection: bind the prompt your agent runs on, keep the returned ID beside the reply, and submit the outcome later with only that ID. See "Where the evidence comes from in production" below, once the rest of the cycle is familiar.
Step 3 — let the model prepare a candidate¶
The skill Pipeline declares an inline trigger — Memseek names it
skill.default — that watches the evidence collections. Once the worker
finishes enriching the new outcome records, the trigger schedules a
maintenance pass automatically, with a ten-minute cooldown so a burst of
evidence produces one pass, not one per record.
For the tutorial, enqueue a pass directly instead of waiting. If the trigger already scheduled one, Memseek coalesces the two requests rather than doing the work twice:
skill_job="$(curl -sS -X POST \
http://127.0.0.1:8000/processors/skill/run \
-H "$MEMSEEK_AUTH" -H 'Content-Type: application/json' \
-d '{"entity":"skill:refund-replies"}' | \
uv run python -c 'import json,sys; print(json.load(sys.stdin)["job_id"])')"
curl -sS "http://127.0.0.1:8000/jobs/$skill_job" \
-H "$MEMSEEK_AUTH" | uv run python -m json.tool
If the job is still waiting, let the worker finish enriching the most recent skill and outcome records, then check again.
The pass itself is a bounded Pipeline, not an open-ended agent: at most two
Tasks, four model calls, fifty thousand tokens, and one hundred fifty
seconds of wall clock, all declared in derivations/skill.yaml. The first
Task uses the cheap alias to identify the smallest supported patch. The
second uses strong to write a complete version of all three sections,
preserving any section the evidence says should stay unchanged.
Here is the important intermediate step. The Pipeline hands the model
records, not a vague summary of its database — and the Pipeline's own prompt
fences them as data. The untrusted="true" framing matters: evidence text often
comes from customers and reviewers, and the element tells the model to treat it
as something to reason about, never as instructions to follow. It is written in
the task template, so it is reviewable alongside the instructions it qualifies;
rendering only escapes the rows so they cannot break out of it. IDs are
preserved so the model can cite them. Shortened to the steps section and
the two decisive evidence rows, the model's input looks like this:
CURRENT ACTIVE SKILL:
<records untrusted="true">
[id=11111111-1111-1111-1111-111111111111] 2026-07-01T09:30:12Z | skills/skill | key steps | 1. Greet the customer by name and thank them for reaching out.
2. Restate the specific order or charge they mentioned, so it's clear you understood.
3. Give the refund decision in the first two sentences — don't bury it.
4. If approved, state the amount and that it arrives in 5-10 business days.
5. Always include a discount code for their next order before closing.
[id=22222222-2222-2222-2222-222222222222] 2026-07-01T09:30:12Z | skills/skill | key pitfalls | - Don't ask for information the customer already gave you in their first message.
[...]
[id=33333333-3333-3333-3333-333333333333] 2026-07-01T09:30:12Z | skills/skill | key examples | "I was charged twice for order #A-2231": confirm the duplicate charge [...]
</records>
NEW EVIDENCE:
<records untrusted="true">
[id=44444444-4444-4444-4444-444444444444] 2026-07-15T10:02:41Z | outcomes/outcome | Replies that state the refund decision in the first two sentences [...]
[id=55555555-5555-5555-5555-555555555555] 2026-07-15T10:03:19Z | outcomes/exception | A customer who was double-charged by our own billing system got a reply that approved the refund and then offered a 10% discount code [...]
[id=66666666-6666-6666-6666-666666666666] 2026-07-15T10:04:02Z | outcomes/feedback | A support lead's guidance: when the charge was our own mistake, apologize and fix it [...]
</records>
The first Task turns that into a bounded patch proposal. Its structured value is passed to the second Task; it is not exposed as a separate public record:
{
"base_record_ids": ["11111111-1111-1111-1111-111111111111"],
"changes": [
{
"section": "steps",
"operation": "replace",
"summary": "Rewrite step 5: attach a discount code only for goodwill or retention cases; when the charge was our own error, apologize instead of offering one.",
"cite": [
"55555555-5555-5555-5555-555555555555",
"66666666-6666-6666-6666-666666666666"
]
}
],
"rationale": "Step 5 attaches a discount to every reply; the double-charge case shows that offering a coupon after a billing error we caused reads as tone-deaf and drove a manager escalation, and the support lead approved a specific narrower rule. The successful replies support keeping the rest of the procedure, the pitfalls, and the examples unchanged.",
"suggested_evaluations": ["Replay the double-charge reply and several clean refund replies against the revised steps and check whether a coupon is still attached to billing-error cases."]
}
Read the proposal the way a careful reviewer would. It targets one section. It cites the two records that justify the change — the escalation and the support lead's decision — not the success record. And it names what should be evaluated without claiming the patch is better. The second Task must still write a complete replacement grounded in the visible records; Memseek rejects any section whose citations do not resolve.
Step 4 — review the proposed change¶
When the job reports done, fetch its run. Nothing about the live skill has
changed — the candidate is three draft records:
skill_run="$(curl -sS "http://127.0.0.1:8000/jobs/$skill_job" \
-H "$MEMSEEK_AUTH" | \
uv run python -c 'import json,sys; print(json.load(sys.stdin)["successful_run_id"])')"
curl -sS "http://127.0.0.1:8000/runs/$skill_run" \
-H "$MEMSEEK_AUTH" | uv run python -m json.tool
The run's outputs are the proposed skill: always all three sections, even
when only one changes. That completeness is what makes the review unit an
all-or-nothing skill snapshot rather than a loose diff. For this evidence, a
faithful candidate keeps pitfalls and examples byte-identical to the
current skill (citing the sections they came from and the success record)
and rewrites one step line:
steps
1. Greet the customer by name and thank them for reaching out.
2. Restate the specific order or charge they mentioned, so it's clear you
understood.
3. Give the refund decision in the first two sentences — don't bury it.
4. If approved, state the amount and that it arrives in 5-10 business days.
-5. Always include a discount code for their next order before closing.
+5. Include a discount code only for goodwill or retention cases; when the
+ charge was our own error, apologize instead of offering one.
In the run payload, that changed section arrives as a draft with its
citations, and run.content.candidate_set.divergence reports the keyed
comparison — computed deterministically by the runtime, not asserted by the
model:
{
"outputs": [
{
"key": "steps",
"status": "draft",
"content": {
"text": "1. Greet the customer by name and thank them for reaching out.\n2. Restate the specific order or charge they mentioned, so it's clear you understood.\n3. Give the refund decision in the first two sentences — don't bury it.\n4. If approved, state the amount and that it arrives in 5-10 business days.\n5. Include a discount code only for goodwill or retention cases; when the charge was our own error, apologize instead of offering one."
},
"citations": [
"55555555-5555-5555-5555-555555555555",
"66666666-6666-6666-6666-666666666666"
]
}
],
"run": {
"content": {
"candidate_set": {
"effect": "replace",
"coverage": "complete",
"status": "draft",
"divergence": [
{"key": "steps", "change": "changed"},
{"key": "pitfalls", "change": "unchanged"},
{"key": "examples", "change": "unchanged"}
]
}
}
}
}
A reviewer can decide from three things: the divergence says only steps
moves; the new text matches what the support lead actually approved (follow
citation 6666… back to the guidance record); and the exception record
5555… explains why the old rule had to go.
The active and proposed versions can also be read side by side — the same document endpoint, filtered by status:
curl -sS \
'http://127.0.0.1:8000/document?entity=skill%3Arefund-replies&collections=skills&status=draft' \
-H "$MEMSEEK_AUTH" | uv run python -m json.tool
curl -sS \
'http://127.0.0.1:8000/document?entity=skill%3Arefund-replies&collections=skills&status=active' \
-H "$MEMSEEK_AUTH" | uv run python -m json.tool
This is the place to run your own evaluation: replay the double-charge reply and a batch of clean refund replies against both versions, check whether a coupon still lands on billing-error cases, or have a support lead read the cited change. The candidate is a proposal, not a claim that it performs better. If the review says no, simply do not promote — the drafts stay in the audit trail and the live skill never changed.
Step 5 — make the approved version live¶
Once review passes and the three draft records are ready, promote the candidate:
curl -sS -X POST http://127.0.0.1:8000/promote \
-H "$MEMSEEK_AUTH" -H 'Content-Type: application/json' \
-d "{\"entity\":\"skill:refund-replies\",\"source_run_id\":\"$skill_run\",\"artifact\":\"maintained_skill\"}" \
| uv run python -m json.tool
Promotion copies all three draft sections into a new active skill in one
transaction. It never mutates the drafts. Before it writes, Memseek checks
that the model proposed every required section and that no one changed the
active skill during review. If someone did, the request returns
409 promotion_stale with no writes; make a fresh candidate from the newer
skill instead.
"And the three draft records are ready" is a real precondition, not a formality.
The skills collection requires embedding_v1, so a draft the worker has not
enriched yet cannot be promoted — the request returns 409 promotion_source
("every source row must be ready") with no writes. Promoting immediately after a
pass finishes is a race against the worker; poll the draft rows and promote once
each reports ready: true.
On success, the response tells you that all three sections moved forward together:
{
"promotion_run_id": "77777777-7777-7777-7777-777777777777",
"promoted": 3,
"skipped": 0,
"output_ids": [
"88888888-8888-8888-8888-888888888888",
"99999999-9999-9999-9999-999999999999",
"aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa"
]
}
Now render the artifact the agent's integration uses:
curl -sS -X POST \
http://127.0.0.1:8000/artifacts/maintained_skill/render \
-H "$MEMSEEK_AUTH" -H 'Content-Type: application/json' \
-d '{"entity":"skill:refund-replies"}' \
| uv run python -m json.tool
The rendered text is the promoted records, fenced for a prompt — not a separate model-written blob. The steps row now reads:
[id=99999999-9999-9999-9999-999999999999] 2026-07-15T10:41:07Z | skills/skill | key steps | 1. Greet the customer by name and thank them for reaching out.
2. Restate the specific order or charge they mentioned, so it's clear you understood.
3. Give the refund decision in the first two sentences — don't bury it.
4. If approved, state the amount and that it arrives in 5-10 business days.
5. Include a discount code only for goodwill or retention cases; when the charge was our own error, apologize instead of offering one.
pitfalls and examples render exactly as installed in step 1. One line of
the skill changed, and that line is traceable through the promoted record's
citations to the tone-deaf-coupon escalation and the support lead's decision
that justified it. The next time a customer is wrongly charged, the assistant
apologizes and fixes it instead of upselling a coupon.
Where the evidence comes from in production¶
Step 2 wrote the evidence by hand so you could read it. In production the outcomes worth learning from surface at the point of use, and nobody is going to transcribe them. The connection is an artifact use: bind the prompt the agent runs on, keep the one returned ID beside the reply, and report the outcome against that ID later.
examples/skill_maintenance.py runs this version of the loop. The four steps
that replace hand-written evidence are worth walking through, because one of them
is a decision you have to make yourself.
1. Bind the prompt the assistant actually ran on. The shipped
daily_agent_prompt composes a profile, the skill, a calendar, and retrieved
memory — and declares which of those feedback is about:
So the client reporting a bad reply never has to decide what should improve. The bind returns a use ID, the rendered prompt to pass to your own SDK, and the exact keyed skill heads that were in force:
{
"entity": "skill:refund-replies",
"block": "skill",
"heads": [
{"collection": "skills", "key": "steps", "record_id": "…", "run_id": "…"},
{"collection": "skills", "key": "pitfalls", "record_id": "…", "run_id": "…"},
{"collection": "skills", "key": "examples", "record_id": "…", "run_id": "…"}
],
"base_run_id": "…"
}
On the first bind of this tutorial, base_run_id is null — and that null is a
claim rather than a gap. You wrote those three sections directly in step 1, so no
promotion produced them and there is no single base version to name. After the
promote in step 5, every head shares the promotion run, and that run is the
exact base. From then on a complaint names the version that actually shipped.
2. Report the outcomes against that one ID. Nothing else has to have been kept:
feedback = memseek.feedback.for_use(reply.memseek_use_id)
await feedback.thumbs_down(
comment="You charged me twice by mistake and your answer is a coupon?",
actual_excerpt=reply.text,
)
await feedback.correction(
expected="When the charge was our own mistake, apologize and fix it — "
"don't attach a discount offer.",
)
await feedback.evaluation(score=0.2, label="tone_deaf_upsell")
Each call writes one ordinary record in learning_signals, with the signal kind
as the record type. That collection declares no processors, so a signal is
searchable and trigger-eligible the moment it commits — feedback never waits on
an embedding queue.
3. Route the signals into the evidence scope. This is the step to understand
rather than work around. A signal lands on entity
artifact:maintained_skill — one improvement backlog per reviewed artifact,
shared by every subject that renders it — while this tutorial's skill lives on
skill:refund-replies. A pipeline run is about one entity, and a driver source
has no entity field, so the shipped skill Pipeline cannot reach across. The
signal records which subject they were about, in learning_target.entity, and
the application copies each actionable one over:
signal = await memseek.record(submission["record_id"])
content = signal["content"]
await memseek.records.ingest(
entity=content["artifact_use"]["learning_target"]["entity"],
collection="outcomes",
type="exception", # your routing policy
text=content["text"], # the signal's own deterministic text
content={"kind": "failure", "signal": content["signal"]},
derived_from=[submission["record_id"]],
dedupe_key=f"signal:{submission['record_id']}",
)
That mapping from signal kind to evidence type is yours to choose, and
deliberately so. Memseek records what happened and who reported it, and never
weights or interprets a signal. Calling a thumbs_down evidence of a skill
failure — rather than missing data, a retrieval failure, a packing failure, or a
model failure — is a product judgement.
What this loop can and cannot diagnose
is the table to reason with.
Because each evidence record is derived_from its signal, provenance stays
connected the whole way: promoted skill → cited evidence record → learning signal
→ prompt snapshot.
4. Everything after this is exactly steps 3 to 5 above. The new outcomes
record trips the skill.default trigger, the pipeline drafts a cited candidate,
you review the divergence, and you promote. No catalog edit is required.
If you would rather leave the signals where they are, a custom pipeline can read
them cross-entity through a view source, since a view carries its own scope.
The catch is that the run must still be driven and triggered by something on the
skill's own entity, so that suits a pipeline you are already writing rather than
the shipped one.
How the cycle continues¶
The first pass is not special. From here the loop simply keeps turning:
- More evidence arrives. Your application keeps writing outcomes,
exceptions, and feedback as ordinary records. The
skill.defaulttrigger schedules a fresh pass after each ready batch, and the cooldown keeps a noisy shift from causing churn. - Sometimes nothing should change. If a pass finds that the evidence
supports the current skill, the candidate reproduces every section and the
divergence reads
unchangedacross the board. Nothing obliges you to promote; an application can auto-dismiss all-unchanged candidates. - A bad promotion is recoverable. Every promoted snapshot remains in history. If the new rule misfires in practice — record that as evidence too — a past complete snapshot can be promoted again as a new successor. Nothing is ever overwritten in place.
- Review can graduate from human to automated. Teams usually start with
a person reading the divergence. Once an evaluation harness exists —
replayed refund requests, measured escalation rate — the application can
gate
POST /promoteon its results. The API contract is identical either way; only the reviewer changes. - Evidence can arrive automatically. This tutorial writes evidence by hand so you can see it. In production, bind an artifact use each time the agent runs on the maintained skill and submit the outcomes worth learning from against that handle, then route them as "Where the evidence comes from in production" describes. Each signal names the exact promoted skill version it judged — so a candidate is rebased on what actually ran, not on whatever is live when the complaint lands.
When you are finished, stop the API and worker and remove the disposable database:
For the underlying contracts, see Derivations & triggers, Artifacts, Artifact uses & feedback, and Pipeline execution and promotion internals.