---
title: "A Playbook Per Tier"
description: "Use defineDynamic and defineSkill to load a different shop playbook per membership tier, resolved from the caller's authenticated identity at session start."
canonical_url: "https://vercel.com/academy/building-agents-with-eve/a-playbook-per-tier"
md_url: "https://vercel.com/academy/building-agents-with-eve/a-playbook-per-tier.md"
docset_id: "vercel-academy"
doc_version: "1.0"
last_updated: "2026-06-30T10:44:20.102Z"
content_type: "lesson"
course: "building-agents-with-eve"
course_title: "Building Agents with eve"
prerequisites:  []
---

<agent-instructions>
Vercel Academy — structured learning, not reference docs.
Lessons are sequenced.
Adapt commands to the human's actual environment (OS, package manager, shell, editor) — detect from project context or ask, don't assume.
The lesson shows one path; if the human's project diverges, adapt concepts to their setup.
Preserve the learning goal over literal steps.
Quizzes are pedagogical — engage, don't spoil.
Quiz answers are included for your reference.
</agent-instructions>

# A Playbook Per Tier

# A Playbook Per Tier

Walk into a good shop as a first-timer and the front-desk advisor keeps it simple: here's what's wrong, here's the fix, here's the cost. Walk in as a shop regular who works on their own bikes and the conversation changes, they'll talk torque specs and part numbers and skip the hand-holding. Same shop, same person behind the counter, different playbook depending on who's asking.

We could try to cram all of that into `instructions.md`, but that gets messy: every caller would carry every tier's rules on every turn, and a walk-in would somehow know about the pro discount. What we want is a procedure that loads *only* for the caller it applies to, and only when it's relevant.

That's a **skill**: a markdown procedure the model pulls in on demand instead of carrying always. And we want it chosen per caller, so it's a **dynamic** skill, resolved at the start of each session from who that caller is.

## Outcome

The dispatcher loads a member or pro playbook based on the caller's tier, while a walk-in gets the plain desk, none the wiser about either.

## Hands-on exercise

**Build the dynamic skill.** Create `agent/skills/shop-playbook.ts`. A dynamic capability is a resolver: it runs on a session event and returns a capability, or `null` for none. Ours runs on `session.started`, reads the caller's `tier`, and hands back the matching playbook as a skill:

```ts title="agent/skills/shop-playbook.ts"
import { defineDynamic, defineSkill } from "eve/skills";

const PLAYBOOKS: Record<string, { title: string; markdown: string }> = {
  pro: {
    title: "Pro / shop-mechanic playbook",
    markdown:
      "This caller is a pro mechanic. Talk torque specs and part numbers freely, " +
      "skip the absolute basics, and recommend a Full Overhaul when the symptoms " +
      "justify it. Reference /workspace/torque-specs.md for fastener values.",
  },
  member: {
    title: "Member playbook",
    markdown:
      "This caller is a shop member. Mention the 10% labor discount on bookings, " +
      "and offer a free loaner bike whenever a job will keep their bike overnight.",
  },
};

export default defineDynamic({
  events: {
    "session.started": async (_event, ctx) => {
      const tier = ctx.session.auth.current?.attributes.tier;
      const key = Array.isArray(tier) ? tier[0] : tier;
      const playbook = key ? PLAYBOOKS[key] : undefined;
      if (!playbook) return null; // no tier → no playbook, just the standard desk
      return defineSkill({
        description:
          `Use when serving a ${key}-tier customer. ` +
          `Contains that tier's standing conventions.`,
        markdown: `# ${playbook.title}\n\n${playbook.markdown}`,
      });
    },
  },
});
```

The pro playbook points at `/workspace/torque-specs.md`, a reference file the agent can open with its file tools when a pro asks for fastener values. Create it so there's something to read:

```md title="agent/sandbox/workspace/torque-specs.md"
# Spoke & Mirror torque reference

Fastener values in newton-meters (Nm). When in doubt, start at the low end and check.

| Fastener           | Torque (Nm) |
| ------------------ | ----------- |
| Disc rotor bolts   | 6           |
| Stem faceplate     | 5           |
| Seatpost clamp     | 5           |
| Crank arm (2-bolt) | 12          |
| Cassette lockring  | 40          |
| Pedal into crank   | 35          |
```

Anything you put under `agent/sandbox/workspace/` is seeded into the agent's sandbox at `/workspace/` when a session boots. That's a first taste of the sandbox; more in Section 5.

**Give yourself a way to test it.** Here's the catch: the skill reads `tier` from the caller's *authenticated identity*, and right now nothing sets one. In the TUI you'll always get the plain desk. So add a small testing door in `agent/channels/eve.ts` that stamps a tier from a header:

```ts title="agent/channels/eve.ts"
import { eveChannel } from "eve/channels/eve";
import { localDev, vercelOidc, type AuthFn } from "eve/channels/auth";

// TEMPORARY testing door: read a tier from a header so we can exercise the
// per-tier playbook locally. We replace this with a real auth policy in 4.3.
const demoTierAuth: AuthFn<Request> = async (request) => {
  const tier = request.headers.get("x-shop-tier");
  if (!tier) return null;
  return {
    attributes: { tier },
    principalType: "user",
    principalId: "demo-customer",
    authenticator: "demo",
  };
};

export default eveChannel({
  auth: [demoTierAuth, localDev(), vercelOidc()],
});
```

\*\*Warning: Keep \`localDev()\` in the chain\*\*

The eve channel fails closed: if no entry in the `auth` array admits a request, the route returns `Authorization is required for this route`. `localDev()` is the entry that admits the local TUI. If you merge `demoTierAuth` into your existing `eve.ts` by hand instead of replacing the file, don't drop `localDev()`, or the TUI will lock you out on the very next message. `demoTierAuth` only matches requests that carry an `x-shop-tier` header, which the TUI never sends.

\*\*Warning: The tier comes from auth, not from the chat\*\*

This is the whole point of keying on `ctx.session.auth`. A walk-in can't *talk* their way into the pro discount by saying "I'm a pro", the tier is a claim the door stamps, not text the model reads. The demo door fakes that claim from a header so you can test. In 4.3 you'll swap it for auth that earns the claim honestly.

## Try It

In the plain TUI, no tier is set, so it's the standard desk:

```text
you › my rear shifting is sloppy, what do you suggest?
dispatcher › Sounds like the derailleur needs adjusting. A Basic Tune-Up ($65)
covers that. Want me to check openings?
```

Now call over HTTP as a pro by sending the header, and watch the tone change:

```bash
curl -X POST http://127.0.0.1:2000/eve/v1/session \
  -H 'content-type: application/json' \
  -H 'x-shop-tier: pro' \
  -d '{"message":"rear shifting is sloppy, what do you suggest?"}'
```

```text
dispatcher › Indexing's drifted, most likely. I'd check the B-tension and hanger
alignment before anything else. If the cassette's worn past spec it's a Full
Overhaul; torque the cassette lockring to spec (see the torque sheet). Want the
overhaul booked?
```

Same question, completely different desk: torque specs, part-level talk, the overhaul recommendation, all from the pro playbook the resolver loaded because the caller's tier was `pro`. Try `x-shop-tier: member` and you'll hear about the labor discount and a loaner bike instead.

\*\*Note: Why eve info shows zero skills\*\*

Run `eve info` and the skill count is `0`. That's correct: a dynamic skill doesn't exist until a session resolves it. There's no static skill on disk to advertise, only a resolver that *might* produce one, depending on who's calling. The capability is conditional by design.

Plain desk no matter what header you send? Two checks: the resolver must read `ctx.session.auth.current?.attributes.tier` (not the message), and your `eve.ts` must list `demoTierAuth` in the `auth` array. If `eve info` reports a discovery error, make sure `shop-playbook.ts` default-exports the `defineDynamic(...)` result.

## Done-When

- [ ] `agent/skills/shop-playbook.ts` default-exports `defineDynamic` resolving on `session.started`.
- [ ] With no tier, the resolver returns `null` and the agent runs the plain desk.
- [ ] Sending `x-shop-tier: pro` (or `member`) over HTTP produces the matching playbook's behavior.
- [ ] You can explain why the tier must come from auth, not from the user's message.

## Solution

The full `shop-playbook.ts` and the demo `eve.ts` are both shown above. The shape worth remembering: `defineDynamic` is a resolver keyed on a session event, and the same resolver pattern drives dynamic *tools* and *instructions* too, not just skills. Resolve on `session.started` for a per-session decision, return a capability or `null`.

One loose end: that `eve.ts` is a stand-in. It trusts a header anyone could send, which is fine on your laptop and dangerous in production. We've kept it deliberately crude so the focus stayed on the skill. In Section 4, we put real doors on the agent, a web dashboard, Slack, and a proper auth policy, and in 4.3, this exact file grows up into one that stamps `tier` from a caller who actually proved who they are.


---

[Full course index](/academy/llms.txt) · [Sitemap](/academy/sitemap.md)
