Describe the flow. Then just run it.
specbook init puts a guide in your project and points your agent's instruction file at it, and specbook add reads your spec's security and sets up its credentials. A coding agent then writes requests and flows the way your team would: typed from the spec, checked, and runnable on mock data. This page is one real run, against Plaid's spec with its 350 operations.
Set up the project
Install Specbook, run specbook init, then specbook add the spec. Adding a spec downloads it and reads its security schemes.
npm install --save-dev @specbook/cli @specbook/corespecbook init # config, a tsconfig and an agent guide; adds typescript and faker to package.jsonnpm install # fetches what init addedspecbook add plaid --spec https://raw.githubusercontent.com/plaid/plaid-openapi/master/2020-09-14.yml --base-url https://sandbox.plaid.comPlaid asks for three headers, so add wrote one profile that sends all three, and a secret for each. Plaid accepts OAuth too, so it wrote a profile for that, and one more that fetches a token with a client id and secret.
✓ downloaded https://raw.githubusercontent.com/plaid/plaid-openapi/master/2020-09-14.yml → openapi/plaid.yml (3.3 MB, sha256 ea443cd78f6f)✓ added "plaid" to .specbook/specbook.config.ts✓ credentials from the spec: 3 ways to authenticate, the first is the default profile "plaid" sends PLAID_CLIENT_ID, PLAID_SECRET, PLAID_VERSION profile "plaid-oauth2" takes an access token in PLAID_OAUTH2 profile "plaid-oauth2-client" signs in server-side with PLAID_OAUTH2_CLIENT_ID and PLAID_OAUTH2_CLIENT_SECRET set the secrets as environment variables or in .specbook/environments/<env>.secrets.env; the mock needs none Plaid-Version reads like a version, not a credential: put its value in PLAID_VERSION plaid-oauth2-client needs a server to hold its secret (dev, test and bundle runs); a static portal offers plaid-oauth2 instead✓ other servers became environments in .specbook/environments/: sandbox.env plaidUrl=https://sandbox.plaid.com✓ plaid: 350 operations → ~/my-api/.generated/plaidauth: { plaid: { // from the spec's security: clientId, secret, plaidVersion title: 'API keys', scheme: ['clientId', 'secret', 'plaidVersion'], apply: { headers: { 'PLAID-CLIENT-ID': '{{secrets.PLAID_CLIENT_ID}}', 'PLAID-SECRET': '{{secrets.PLAID_SECRET}}', 'Plaid-Version': '{{secrets.PLAID_VERSION}}', }, }, },services: { plaid: { spec: './openapi/plaid.yml', baseUrl: '{{plaidUrl}}', auth: ['plaid', 'plaid-oauth2', 'plaid-oauth2-client'], },The first profile is the default, and the service lists them all. The values come from your environment, never from a file, and the mock needs none. add also says what it left for you: Plaid-Version reads like a version and not a credential.
Ask for a request
That is the whole setup. This is the whole prompt.
Add a request that creates a Plaid sandbox public token.
The agent read the guide and the spec, then wrote one request. It left the config alone, because the credentials were already there. It captures the public token, because the next step will need it.
import { SandboxPublicTokenCreate } from '#api/plaid'
export default SandboxPublicTokenCreate.request({ name: 'Create a sandbox public token', tags: ['smoke'], // First Platypus Bank, one of Plaid's Sandbox test institutions. data: { institution_id: 'ins_109508', initial_products: ['transactions'], }, capture: { publicToken: (res) => (res.status === 200 ? res.data.public_token : undefined), }, assert: (res, { assert }) => { assert.is(res.status, 200, 'public token created') if (res.status === 200) { assert.type(res.data.public_token, 'string', 'response has a public_token') assert.match(res.data.public_token, /^public-/, 'token looks like a public token') } },})Ask for a flow
The second prompt says what should end up working. It does not list the calls.
Now make a flow that links a bank account in the Plaid sandbox and reads back its accounts and transactions.
The agent found the four operations in the spec, wrote a request for each so every step also runs on its own, and chained them in one flow.
import { sequence } from '@specbook/core'import getAccounts from '../requests/plaid/accounts-get.req'import exchangeToken from '../requests/plaid/item-public-token-exchange.req'import createPublicToken from '../requests/plaid/sandbox-public-token-create.req'import getTransactions from '../requests/plaid/transactions-get.req'
const isoDay = (d: Date) => d.toISOString().slice(0, 10)
export default sequence({ id: 'link-bank-account', title: 'Link a bank account in the Plaid sandbox', description: 'Create a sandbox public token, exchange it for an access token, then read back the accounts and transactions.', tags: ['plaid', 'smoke'],}) .step('Create a sandbox public token', createPublicToken) // captures `publicToken` .step('Exchange the public token', (ctx) => exchangeToken.with({ data: { public_token: ctx.captured.publicToken ?? '' } }), ) // captures `accessToken`, `itemId` .step('Get the accounts', (ctx) => getAccounts.with({ data: { access_token: ctx.captured.accessToken ?? '' } }), ) // captures `accountIds` .step('Get the transactions', (ctx) => getTransactions.with({ data: { access_token: ctx.captured.accessToken ?? '', start_date: isoDay(new Date(Date.now() - 30 * 24 * 60 * 60 * 1000)), end_date: isoDay(new Date()), }, }), ) // captures `transactionAccountIds`, `totalTransactions` .check('Transactions belong to the linked accounts', (ctx, { assert }) => { for (const id of ctx.captured.transactionAccountIds) { assert.ok( ctx.captured.accountIds.includes(id), `transaction account ${id} is one of the linked accounts`, ) } })Values pass by name, and the names are checked
The first request captures publicToken. The next step sends it as public_token, the exchange captures accessToken, and the last two steps send that. The agent wrote each hand-off as ctx.captured.publicToken, not as a {{publicToken}} string.
A string compiles whatever it says. ctx.captured.publicToken has the type the capture returned, and the field it lands in is typed from the spec, so a wrong name on either side is an error in your editor. A .check step proves the chain: every transaction it reads is on one of the accounts it read.
Rename a capture
Rename publicToken to publicTokenX in the first request, then run the compiler and the tests.
$ npx tsc --noEmitsequences/link-bank-account.seq.ts(17,61): error TS2551: Property 'publicToken' does not exist on type '{ publicTokenX: string | undefined; }'. Did you mean 'publicTokenX'?
$ npx specbook test --grep "Link a bank" Flows 1 passed (1) Steps 5 passed Assertions 12 passedThe compiler points at the broken step. The test run still passes on the mock, so a green run does not prove the names line up. That is why the guide ends with npx tsc --noEmit, and why the agent reads values from ctx and not from strings.
It runs on mock data
npx specbook test runs the flow against the mock, which answers from the spec's examples. No server and no keys.
specbook test 0.12.0 env mock · 1/5 tests
✓ Link a bank account in the Plaid sandbox › Create a sandbox public token POST /sandbox/public_token/create 200 1ms ✓ Link a bank account in the Plaid sandbox › Exchange the public token POST /item/public_token/exchange 200 0ms ✓ Link a bank account in the Plaid sandbox › Get the accounts POST /accounts/get 200 1ms ✓ Link a bank account in the Plaid sandbox › Get the transactions POST /transactions/get 200 0ms ✓ Link a bank account in the Plaid sandbox › Transactions belong to the linked accounts
Flows 1 passed (1) Steps 5 passed Assertions 12 passed Duration 8msSetup took a few seconds, and the agent needed about two minutes to get from there to a passing flow, with no fixes by hand. That was one run, so yours will vary. Running it against Plaid's live sandbox needs a free account's keys, set as PLAID_CLIENT_ID, PLAID_SECRET and PLAID_VERSION, and that part is not tested here.
Do the same in your project
Run specbook init and your agent has the guide. It also writes a short Specbook section to your AGENTS.md, so agents that read that file open the guide before they touch a request. Then specbook add your spec, and its credentials are set up too.
npx specbook init # writes .specbook/agent-guide.md, and an AGENTS.md if you have nonenpx specbook init --agents # adds the Specbook section to an AGENTS.md or CLAUDE.md you already haveAgents that read from the web can fetch /llms.txt, which is the same guide with links to these pages. Write your own prompts the way these were written: say what should end up working, and name the API in plain words. The agent finds the operations in the spec.