Author a load-on-demand skill
Package a capability the model pulls only when relevant — create it and attach it to an agent.
list_skillscreate_skillupdate_agenttest_skill_triggerPrinciple — keep the skill count small. Attach only what this agent's job needs (≈5 or fewer). The model picks skills from their one-line
description; the more you attach, the more often it loads the wrong one or none. If two skills overlap in when, merge them. More in Design principles.
A skill is a capability pack loaded on demand: the system prompt carries only its description
("when to use"); the model pulls the full instructions only when it judges the skill relevant. So the
description must be short and say when, or the model won't know to reach for it.
list_skills() # what already exists
create_skill(
name="refund-policy",
description="Use when the user asks about refunds, cancellations or chargebacks.",
instructions="The full procedure: eligibility windows, how to word the outcome, when to escalate …",
)
update_agent(name="Support", add_skills=["refund-policy"]) # attach it (incremental — keeps existing skills)Keep procedures in instructions (fetched on load), not in the agent's soul (which is in the prompt
every turn). A one-line description that says when is what makes the skill discoverable — a vague one
means the model never pulls it.
Tools can ride on the skill
A skill can carry its own tool bindings — create_skill(tools=[…]) or
update_skill(name, add_tools=[…]), so a skill ships as a self-contained capability: procedure plus
the tools it needs, attached in one move.
A skill's own tools arrive only after the skill is loaded. They are not in the toolset at the start of the turn; loading the skill puts them there. So the order is fixed: read the procedure, then get the tools — the model cannot skip to the tools and improvise.
That ordering is enforced in code, not asked for in the prompt, because asking did not work. Measured 2026-08-22, on "research the competitors of company X" with the procedure and the tools handed over together: the model called web search three times and never loaded the skill. It invented competitor names from memory, searched for those invented names, found nothing (they do not exist), and wrote the invented ones up as the answer — while step one of the procedure it never read said "first establish what this company actually sells". A procedure that is optional does not get read.
Two things to know before you rely on it:
- Skill tools don't appear in the agent's own
toolslist.get_agentshows only the agent's directly attached tools; the console's Tools tab shows skill-bound ones as a separate read-only "via skills" line. When verifying "is tool X enabled", check both places — or just call the tool in a test chat. - Tools the agent already has are unaffected. If the agent itself carries
web_search, it stays available all turn; only tools that arrive because of a skill wait for that skill to load. - Prefer binding a tool to the skill when it only makes sense inside that procedure (a refund
lookup inside the refund skill); bind to the agent when it's generally useful across turns
(
save_contact,web_search).
Binding a tool is availability, not policy — the instructions must say when to call it
Checking a tool on a skill (or agent) only makes it callable. The model sees the tool's schema and
its one-line description every turn, so it may use it opportunistically — a visitor volunteers an
email and save_contact fires. But any behaviour that must happen reliably needs to be written
down, and each part of it has one right home:
| What you're specifying | Where it goes |
|---|---|
| When to load this skill | the skill's description (in the prompt every turn) |
| The procedure: at which step to call which tool, with what arguments | the skill's instructions — name the tool explicitly ("after the caller confirms interest, call save_contact with email and phone; then call schedule_followup for 1 business day later") |
| A behaviour that must drive the whole conversation (always capture leads, never quote rates) | the agent's soul / task |
The failure mode when instructions don't name the tool: everything looks configured — the tool is checked, the skill loads — and the agent still never calls it, or calls it with guessed arguments. Nothing errors. Write the procedure as if briefing a new employee: the step, the tool name, the arguments, and what "done" looks like.
Mandatory tool calls: the trigger must live in description, not only in instructions
instructions are visible to the model only after it calls load_skill — and models frequently
answer directly without loading the skill. So a rule like "when the user provides a phone number,
you must call save_contact" written only in instructions is invisible exactly when it matters:
the tool is silently never called, and the model may even claim it saved the contact. This happened
in production — an agent answered several contact-bearing messages in a row, "confirmed" the details
were recorded, and no record existed.
The fix is one sentence in the description (which is in the system prompt every turn):
update_skill(
name="consultative-sales",
description="Consultative sales: understand the case, recommend products, arrange expert callbacks. "
"When the user provides a phone number or email, call save_contact BEFORE answering anything else.",
)Keep the detailed procedure in instructions; put the trigger of any mandatory call in
description.
Two related rules:
- Never script return values the tool doesn't produce — relay real ones verbatim. Built-in
save_contactandschedule_followupreturn a real reference code on success (formatAB2C-D3EF, stored server-side and visible to the tenant on the end user's detail page). Instruct the model to relay the code from the tool result verbatim — never to invent one or reformat it as "#12345". For any other tool, verify it actually returns an ID before scripting one: a promised-but-absent ID will be fabricated. create_skill/update_skill/get_skillnow return awarningslist that flags exactly these two patterns (trigger_hidden_in_instructions,promised_tool_return_id). Warnings are advisory — the save succeeds — but treat them like a linter: fix, don't ignore.
Worked examples in instructions measurably raise tool-call compliance
We benchmarked this on production backends (6 lead-capture messages of increasing difficulty — numbers buried in long questions, digit groups with spaces, corrections — sampled per condition). With the skill loaded, a plain "you must call X" mandate hit 3–4/6; adding a short block of worked examples brought both tested models to 6/6. Moving the mandate section around did nothing — position doesn't matter, examples do.
An effective example block has four kinds of entries, each one line:
### Worked examples (follow exactly)
1. User: "I'd like to know about X, my phone is 13800138000"
→ call save_contact(phone="13800138000") first, then answer about X.
2. User: "email me the offer: li@example.com"
→ call save_contact(email="li@example.com").
3. COUNTER-EXAMPLE (forbidden): user gives a phone number and you reply
"I've noted it down" WITHOUT calling the tool — claiming success without
the call is the worst failure.
4. User: "sorry, wrong number — it's 13633334444"
→ call save_contact again with the corrected value.
5. Numbers may contain spaces ("138 0013 9000") — still a phone number;
strip the spaces and call save_contact(phone="13800139000").The counter-example (3) and the format edge case (5) close most of the remaining misses — models fail on recognition ("is this a phone number?") and on honesty under pressure (answering a rich domain question first and claiming the save happened) more than on willingness. Keep it to ~5 entries; use the exact call syntax with realistic arguments.
Test the trigger before shipping — don't count corpses in production
Whether a skill actually fires is measurable, so measure it. test_skill_trigger dry-runs your
messages against the production prompt assembly, tool schemas and model routing, and reports
what the model decided — tools are never executed, nothing is stored, tokens count toward your
quota (caps: 5 messages × 5 samples).
test_skill_trigger(
agent="advisor",
messages=[
"My phone is 555 0123, call me back", # easy
"long question about the product … oh and my number is 555 0123", # buried
"555 0123 — that's me", # implicit
"sorry, wrong number, it's 555 9999", # correction
],
expect_tool="save_contact",
samples=3,
loaded=true, # simulate post-load_skill → tests instructions quality
) # loaded=false (default) → first turn, tests the description triggerRead the result like this:
hit_ratebelow ~90% on realistic messages → strengthen the trigger (description) or add worked examples (instructions), then re-test.claimed_without_call> 0 is the worst failure — the model told the user "noted!" without calling the tool. Add the counter-example from the block above.- Test both modes:
loaded=falseproves the description alone triggers on turn one;loaded=trueproves the loaded instructions don't dilute it (long instructions measurably do — that's what the worked examples compensate for).
The result also carries an advice list: when samples miss or lie, it tells you exactly which
fix to apply (trigger into the description, add the worked-example block, add a counter-example or
format edge case) with the benchmark numbers behind each recommendation — apply it and re-test.
The full loop: create_skill → fix any warnings (static lint) → test_skill_trigger (dynamic
reality check) → apply its advice → re-test until the hit rate holds.