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_KEY environment variable. See Authentication.
  • Node.js 18 or later, with "type": "module" in package.json. Nothing to install.
  1. 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.

  2. 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.js
    import { 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.`);
    Terminal
    export CRACKLE_API_KEY="ck_your_key_here"
    export TOOL_TOKEN="your-endpoint-token"
    node sync-agent.js agent.json
  3. Run 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.yml
    name: 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. variables is 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.