Pinecall

Example: Headless Agent

Complete runnable example — a phone support agent with zero web server.

Run it with pinecall run agent/index.js.

What it does#

agent/index.js is a phone support agent for an e-commerce store. It answers calls, looks up orders by phone number, and handles returns.

The complete file#

// agent/index.js
import { Pinecall, tool } from "@pinecall/sdk";
import { z } from "zod";
import { promises as fs } from "node:fs";

const pc = new Pinecall();

const lookupOrder = tool({
  name: "lookupOrder",
  description: "Look up the customer's most recent order.",
  schema: z.object({
    phone: z.string().describe("Customer phone number"),
  }),
  execute: async ({ phone }) => {
    // your logic — query your database, etc.
    return { orderId: "ORD-1234", status: "shipped", eta: "tomorrow" };
  },
});

export const agent = pc.agent("support", {
  voice: "elevenlabs/sarah",
  language: "en",
  llm: "openai/gpt-5-chat-latest",
  stt: "deepgram/flux",
  prompt: `You are a support agent for an online store.
Help customers check order status and process returns.
Be friendly, brief, and professional.`,
  phoneNumber: "+13186330963",
  greeting: "Hi! Thanks for calling. How can I help you today?",
  tools: [lookupOrder],
});

// Log every call to disk
agent.on("call.ended", async (call, reason) => {
  await fs.appendFile("./calls.jsonl", JSON.stringify({
    id: call.id, from: call.from, duration: call.duration,
    reason, endedAt: new Date().toISOString(),
  }) + "\n");
});

Run it#

pinecall run agent/index.js

You'll see the boot banner with agent name, model, and phone number. When calls come in, the runner displays a live transcript with tool calls.

That's it. No web server, no token endpoint, no frontend. The agent answers calls to +13186330963 and logs every call to calls.jsonl. When the LLM calls lookupOrder, the SDK validates the args with Zod and runs the execute function automatically.

Adding more tools#

Just define more tool() objects and include them in the array:

const processReturn = tool({
  name: "processReturn",
  description: "Start a return process for an order.",
  schema: z.object({
    orderId: z.string().describe("The order ID to return"),
    reason: z.string().describe("Reason for the return"),
  }),
  execute: async ({ orderId, reason }) => {
    // your logic — create a return ticket, etc.
    return { returnId: "RET-001", status: "initiated" };
  },
});

const agent = pc.agent("support", {
  // ...same config
  tools: [lookupOrder, processReturn],
});

Adding WhatsApp#

Same headless pattern — add a channel:

agent.addWhatsapp({
  phoneNumberId: process.env.WA_PHONE_NUMBER_ID,
  accessToken: process.env.WA_TOKEN,
  appSecret: process.env.WA_APP_SECRET,
});

Now the agent answers both phone calls and WhatsApp messages. Same prompt, same tools, no extra code.

Deploy options#

  • PM2 / systemd — long-running daemon on a server
  • Docker container — one image, multiple instances
  • Fly.io / Railway / Render — managed processes

The agent only needs outbound network access to voice.pinecall.io. No inbound ports, no public IPs.

What's next#