# Two ways to build an agent in code

> Let VoiceLayer run the agent loop with tools written like Vercel AI SDK tools, or run the loop yourself and bring it as the brain of a VoiceLayer agent.

Source: https://docs.vlayers.ai/developers/sdks/two-ways-to-build

Kind: Concept

An agent in code is a prompt, a model and tools. The one real choice is who runs the loop: the part that sends the conversation to the model, reads the tool calls it asks for, runs them and goes round again. VoiceLayer runs every call and text in both cases, so speech, telephony, turn-taking and the guard on what the agent says stay with VoiceLayer.

| | **Hosted** | **Bring your own brain** |
|---|---|---|
| Who runs the loop | VoiceLayer | You |
| What you write | `defineAgent` with `tools`, a `model` and `actions` | A framework agent (Claude Agent SDK, OpenAI Agents SDK, Vercel AI SDK, LangGraph) or a function |
| Where your code runs | Your worker runs each tool when the loop calls it | Your worker runs the whole agent |
| The model and its key | Chosen in code, the key saved under Connections | Yours, on your provider account |
| Tools | Your own, plus VoiceLayer's and your connected apps' | Whatever your framework has, plus VoiceLayer's tools |
| Package | `@voicelayer/sdk` | `@voicelayer/sdk` and `@voicelayer/agents` |
| Pick it when | You want the shortest path, and tools in your own code are enough | You already have an agent, or you need a loop VoiceLayer does not run |

You can use both in one workspace, one agent each.

## Hosted: tools written like Vercel AI SDK tools

`defineAgent` reads tools in the shape the Vercel AI SDK uses: a description, an `inputSchema` and an `execute`. The `execute` runs in your worker, on your network, so it can reach a private service. VoiceLayer's loop decides when to call it.

```ts
import { defineAgent, tool } from '@voicelayer/sdk';
import { z } from 'zod';

export default defineAgent({
  name: 'order-desk',
  prompt: 'You answer for Acme. Look an order up before you talk about it.',
  capabilities: ['system.say'],

  model: { provider: 'anthropic', id: 'claude-haiku-4-5' },

  tools: {
    lookup_order: tool({
      description: 'Find an order by its number and return its status.',
      inputSchema: z.object({ orderId: z.string() }),
      execute: async ({ orderId }) => db.orders.find(orderId),
    }),
    refund_order: tool({
      description: 'Refund an order in full.',
      inputSchema: z.object({ orderId: z.string() }),
      needsApproval: true,
      execute: async ({ orderId }) => db.orders.refund(orderId),
    }),
  },

  actions: ['vl_check_availability', { name: 'vl_send_text', confirm: true }],
});
```

`@voicelayer/sdk` does not depend on the `ai` package. A tool from `ai`'s own `tool()` works in the same place, and so does the `tool` exported here, which needs nothing but `zod`.

### Tools

- **`inputSchema`** is a `zod` schema (version 3 or 4) or the AI SDK's `jsonSchema(...)`. The arguments are checked against it before `execute` runs.
- **`execute`** receives the arguments and the AI SDK's options. Its abort signal is the invocation's, and `experimental_context` is the VoiceLayer context (`ctx.say`, `ctx.metadata`, and the rest). Return anything JSON can hold; throw an error with a message that is safe to say aloud, and the agent can apologise with it.
- **`needsApproval`** (or `approval`) means ask the person first. The agent reads back what it is about to do and runs `execute` only after a yes. A function is read as always ask. On a realtime voice model (`models.realtime`) the tool does not ask first: it runs without asking, and the build warns you when an agent sets both.
- Every run of your tools is recorded with the conversation, with its outcome. The arguments are kept as a digest, never as values.

### Models

`model` is a plain description: `{ provider: 'anthropic', id: 'claude-haiku-4-5' }`, with optional `options` for `temperature`, `maxOutputTokens` and `topP`. The providers are `openai`, `anthropic` and `google`. An AI SDK model such as `anthropic('claude-haiku-4-5')` is also accepted: only its provider and model id are read, and the object never leaves your machine. A key or base URL is refused in code. Save the provider key under Connections in the dashboard and VoiceLayer uses it for this workspace.

### Actions

`actions` names VoiceLayer's own tools (`vl_book_meeting`, `vl_check_availability`, `vl_send_text`, `vl_send_email`) and your connected apps' tools (`int_hubspot_contacts_create`). Each entry is a name, or an object with `confirm`, `sendRealValues` and the `connection` to use.

What you declare is what the agent has. It does not run until an Admin of your workspace switches it on, so a deploy can never quietly widen what an agent may do. If you change a declaration (a different connection, `confirm` turned on), it waits for the Admin again. Until then the agent is told it cannot do that thing and does not promise it. An Admin can still set argument rules and limits, and can switch an action off.

## Bring your own brain

Keep the loop you already have. Your agent becomes the brain of a VoiceLayer agent: VoiceLayer does the listening and speaking, sends each turn to your worker, and says what comes back.

```ts
import { Agent } from '@openai/agents';
import { defineAgent } from '@voicelayer/sdk';
import { openaiBrain } from '@voicelayer/agents/openai';

export default defineAgent({
  name: 'front-desk',
  prompt: 'You are the front desk of Harbor Dental.',
  capabilities: ['system.say', 'system.transfer_call', 'system.end_call'],
  brain: openaiBrain(new Agent({ name: 'front-desk', model: 'gpt-4o-mini', instructions: 'Short, natural sentences.' })),
});
```

The adapters are `openaiBrain`, `claudeBrain`, `aiSdkBrain` and `langchainBrain`, in `@voicelayer/agents`. With the Vercel AI SDK that is your own `streamText` or `ToolLoopAgent` loop. VoiceLayer's tools reach your framework as its own tools, and every call goes back through VoiceLayer's checks. Your model usage is on your provider account.

For texts, `serveDirect` answers the conversations your code controls with the same agent. See [Bring your agent](https://docs.vlayers.ai/developers/frameworks) for each framework and [Brains in code](https://docs.vlayers.ai/developers/sdks/brains) for the details of a voice brain.

## Which one

- You are starting fresh, and your tools are functions: **hosted**.
- You have an agent that already works and you want it on the phone and on text channels: **bring your own brain**.
- You want the agent to use a tool VoiceLayer runs (booking, texting, a connected app) without writing the integration: either. Hosted names it in `actions`; a brain reaches it as a tool.

## Common errors

| Problem | Fix |
|---|---|
| The build says a tool needs `execute` | A hosted tool is an `inputSchema` and an `execute`. A tool with no function to run has nothing for your worker to serve. |
| The build refuses `model` | Use `{ provider, id }` with `openai`, `anthropic` or `google`. Keys belong under Connections, not in code. |
| An action never runs | It is waiting for an Admin to switch it on. Ask one, and check you did not change the declaration since. |

## Next steps

- [Bring your agent](https://docs.vlayers.ai/developers/frameworks): a page for each framework.
- [Brains in code](https://docs.vlayers.ai/developers/sdks/brains): voice brains in detail.
- [Tools](https://docs.vlayers.ai/build/tools): HTTP tools you register once and share between agents.
