Cookbooks · Running it
Keep an agent in your repo
An agent’s instructions are as much a part of your product as its code, and deserve the same treatment: a file in your repository, changes reviewed in pull requests, and a script that makes the workspace match the file.
Before you start
- An agent, and its ID. The Quickstart makes one.
- A workspace API key in the
CRACKLE_API_KEYenvironment variable. See Authentication. - Node.js 18 or later, with
"type": "module"inpackage.json. Nothing to install.
Describe the agent in a file
The file holds what you would send to Create an agent, with the agent’s tools listed alongside. Leave secrets out of it.
agent.json{ "name": "Front desk", "systemInstruction": "You answer calls for Sunrise Dental. Use @check_order_status when a caller asks about an order. Keep replies to one or two short sentences.", "greetingInstruction": "Greet {{customer_name}} warmly in one short sentence.", "voice": { "language": "en", "speed": 1 }, "variables": { "customer_name": { "defaultValue": "there" } }, "systemTools": { "endCall": true }, "tools": [ { "name": "check_order_status", "description": "Looks up an order by its number, which the caller reads out.", "method": "GET", "url": "https://your-app.com/api/orders/{{order_id}}", "params": [{ "key": "order_id", "value": "A-1042" }], "auth": "bearer", "timeoutMs": 5000 } ] }Write tool names in lower case with underscores. That is how they are saved, and the script matches tools by name.
Write the sync script
It finds the agent by name, creates or updates it, then brings the agent’s tools into line with the file.
sync-agent.jsimport { readFile } from 'node:fs/promises'; const API = 'https://goblins-call-api.onrender.com'; const headers = { Authorization: `Bearer ${process.env.CRACKLE_API_KEY}`, 'Content-Type': 'application/json', }; async function api(method, path, body) { const response = await fetch(`${API}${path}`, { method, headers, body: body && JSON.stringify(body) }); const data = await response.json(); if (!response.ok) throw new Error(`${method} ${path}: ${data.error}`); return data; } const { tools = [], ...fields } = JSON.parse(await readFile(process.argv[2] || 'agent.json', 'utf8')); // The file's name is how its agent is found. Keep names unique within the workspace. const { agents } = await api('GET', '/v1/agents'); const existing = agents.find((agent) => agent.name === fields.name); const { agent } = existing ? await api('PATCH', `/v1/agents/${existing.id}`, fields) : await api('POST', '/v1/agents', fields); // Tools are matched by name: changed ones are updated, new ones created, missing ones removed. const { tools: saved } = await api('GET', `/v1/tools?agentId=${agent.id}`); const toolIds = []; for (const tool of tools) { // The endpoint's credential comes from the environment, not from the file. const body = { ...tool, authToken: process.env.TOOL_TOKEN }; const match = saved.find((item) => item.name === tool.name); const result = match ? await api('PUT', `/v1/tools/${match.id}`, body) : await api('POST', '/v1/tools', { ...body, agentId: agent.id }); toolIds.push(result.tool.id); } for (const stale of saved.filter((item) => !tools.some((tool) => tool.name === item.name))) { await api('DELETE', `/v1/tools/${stale.id}`); } await api('PATCH', `/v1/agents/${agent.id}`, { toolIds }); console.log(`${existing ? 'Updated' : 'Created'} ${agent.name} (${agent.id}) with ${toolIds.length} tools.`);Terminalexport CRACKLE_API_KEY="ck_your_key_here" export TOOL_TOKEN="your-endpoint-token" node sync-agent.js agent.jsonRun it when the file changes
With the key stored as a secret, your CI can apply every merged change. On GitHub:
.github/workflows/sync-agent.ymlname: Sync agent on: push: branches: [main] paths: [agent.json, sync-agent.js] jobs: sync: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 20 - run: node sync-agent.js agent.json env: CRACKLE_API_KEY: ${{ secrets.CRACKLE_API_KEY }} TOOL_TOKEN: ${{ secrets.TOOL_TOKEN }}
Try it
Run the script, then open the studio: the agent is there with its tool. Change a line of the instructions in the file and run it again; the script reports Updated and the studio shows the new text. Calls started after that use it.
Things to know
- The file wins. An edit made in the studio is overwritten the next time the script runs. Decide which of the two is the source of truth for each agent and keep to it.
- Updates only change what the file names. A field you delete from the file keeps its saved value; set it to an empty value to clear it.
variablesis the exception and is replaced as a whole. - One file per environment. Give staging and production their own agents, such as “Front desk (staging)”, or their own workspaces with their own keys, so a change can be tried before callers hear it.
- Give the CI its own key. A key named for the job can be revoked without touching the one your servers use.