---
title: "Quick Start"
description: "Install Nuabase and make your first typed get() and list() calls."
canonical: "https://nuabase.com/docs/quick-start/"
---

> Documentation index: https://nuabase.com/llms.txt

This guide gets you from installation to your first typed LLM call.

## Prerequisites

- Node.js 18 or later
- Zod 4 (Nuabase uses Zod 4's built-in JSON Schema conversion)
- One of:
  - A coding agent CLI installed and logged in: [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Codex](https://github.com/openai/codex), or Pi
  - An API key for Groq, Cerebras, Gemini, OpenRouter, or DeepSeek

## Installation

```bash
npm install nuabase zod
```

## Create a client

With the Headless Coding Agent API, no API key is needed. `findLocalAgent()` picks the first agent that is installed and logged in, trying Claude Code first:

```typescript
import { Nua, z } from 'nuabase';
import { findLocalAgent } from 'nuabase/local-agent';

const nua = Nua.direct({ localAgent: await findLocalAgent() });
```

Or, with an LLM provider API key, using the LLM Provider APIs:

```typescript
import { Nua, z } from 'nuabase';

const nua = Nua.direct({
  providers: { openrouter: { apiKey: process.env.OPENROUTER_API_KEY! } },
});
```

Everything below works the same with either client.

## Your first request

Pass only a prompt to get a plain string back:

```typescript
const result = await nua.get('What is the capital of France?');

if (result.success) {
  console.log(result.data); // "Paris"
} else {
  console.error(result.error);
}
```

This is the whole `result` from a run on Claude Code:

```json
{
  "success": true,
  "data": "Paris",
  "usage": {
    "promptTokens": 1571,
    "completionTokens": 53,
    "totalTokens": 1624,
    "promptCacheReadTokens": 0,
    "promptCacheWriteTokens": 1569
  },
  "model": "claude-code:claude-opus-5-5",
  "latencyMs": 3332,
  "source": "direct",
  "meta": {
    "engine": "local-agent",
    "agent": "claude-code",
    "model": "claude-opus-5-5",
    "costUsdEstimate": 0.01362,
    "schemaEnforcement": "native"
  }
}
```

Each call is one agent process, so expect a few seconds per call rather than milliseconds. The default logger also prints a line when each call starts and completes:

```text
[INFO] LLM call started {
  type: 'llm_call_start',
  span_id: '7e42fedf-760f-46b9-bcaa-017a061c3748',
  service: 'claude-code',
  method: 'native',
  binaryPath: '/Users/you/.local/bin/claude',
  model: 'default'
}
[INFO] LLM call completed {
  type: 'llm_call_complete',
  span_id: '7e42fedf-760f-46b9-bcaa-017a061c3748',
  service: 'claude-code',
  method: 'native',
  exitCode: 0,
  usage: { promptTokens: 1571, completionTokens: 53, totalTokens: 1624, promptCacheReadTokens: 0, promptCacheWriteTokens: 1569 },
  duration_ms: 3331
}
```

Pass your own `logger` to [`localAgent()`](/docs/local-agent.md#options) to route or silence these.

## Typed outputs

Pass input data and a Zod schema to get a typed value, parsed by your schema:

```typescript
const AddressSchema = z.object({
  street: z.string(),
  city: z.string(),
  state: z.string(),
  zip: z.string(),
});

const result = await nua.get('Extract the shipping address', {
  input: 'Please ship the order to 123 Main St, Springfield, IL 62701',
  output: { name: 'address', schema: AddressSchema },
});

if (result.success) {
  // result.data is typed as z.infer<typeof AddressSchema>
  console.log(result.data);
  // { street: '123 Main St', city: 'Springfield', state: 'IL', zip: '62701' }
}
```

The `name` field (`'address'`) names the output for the model. Choose a name that describes the value.

## Processing arrays

`nua.list()` processes an array of records in a single request:

```typescript
const LeadInsights = z.object({
  company_name: z.string(),
  industry: z.string(),
  company_size: z.enum(['SMB', 'Mid-market', 'Enterprise']),
  recommended_follow_up: z.enum(['Call', 'Email', 'Event']),
});

const leads = [
  { leadId: 'lead-101', name: 'Acme Analytics', notes: 'Growth-stage SaaS, 80 employees. Wants a live demo next week.' },
  { leadId: 'lead-102', name: 'Bright Logistics', notes: 'Regional freight operator asking for pricing via email.' },
];

const result = await nua.list(
  'Extract company_name, industry, company_size, and recommended_follow_up from each lead.',
  {
    input: leads,
    primaryKey: 'leadId',
    output: { name: 'leadInsights', schema: LeadInsights },
  },
);

if (!result.success) throw new Error(result.error);
console.log(result.data);
```

Output:

```json
[
  {
    "leadId": "lead-101",
    "leadInsights": {
      "company_name": "Acme Analytics",
      "industry": "SaaS / Analytics",
      "company_size": "SMB",
      "recommended_follow_up": "Call"
    }
  },
  {
    "leadId": "lead-102",
    "leadInsights": {
      "company_name": "Bright Logistics",
      "industry": "Logistics / Freight",
      "company_size": "Mid-market",
      "recommended_follow_up": "Email"
    }
  }
]
```

Each returned row carries the `primaryKey` value (`leadId`) next to the output field (`leadInsights`). The model may return rows in a different order than the input, so match rows by key, not by array index.

## Error handling

Calls don't throw for LLM failures. Check `result.success`:

```typescript
const result = await nua.get('Summarize this in one sentence', {
  input: text,
  output: { name: 'summary', schema: z.string() },
});

if (!result.success) {
  console.error(`Failed after ${result.attempts} attempts: ${result.reason}`);
  return;
}

console.log(result.data);
console.log(`${result.usage.totalTokens} tokens via ${result.model}`);
```

On success:

```text
The Q3 release moved exports to a background queue, which cut median export time from 40 seconds to 6, but large workspaces now get exports finishing out of order.
1743 tokens via claude-code:claude-opus-5-5
```

On failure, here with a model name the user's Claude Code account doesn't have:

```text
Failed after 3 attempts: There's an issue with the selected model (claude-nonexistent-9). It may not exist or you may not have access to it. Run --model to pick a different model.
```

`reason` says why the call failed, in the provider's or agent's own words. When the answer never fit your schema, `reason` is the validation error followed by the model's raw response, so show your own message for those. `error` is a one-line summary for logs. A call throws only when its arguments are invalid, before anything is sent. See [API Overview](/docs/api.md#errors) for the full list.

## Hand a bigger job to the agent

When a job needs the agent to work in the user's project rather than return one answer, open an Interactive Coding Agent Session with a first prompt your app writes:

```typescript
import { startAgentSession } from 'nuabase/local-agent';

const session = await startAgentSession({
  agent: 'claude-code',
  cwd: projectDir,
  prompt: 'Add tests for the CSV importer in src/import/csv.ts. Reply with your plan first and wait for me to say go.',
  dir: `${projectDir}/tmp/agent-sessions`,
});

if (session.kind === 'command') {
  console.log(`Run in a terminal: ${session.files.command}`);
}
```

On macOS this opens a new terminal window running Claude Code in the project. See [Interactive Coding Agent Session](/docs/local-agent/sessions.md) for every result and option.

## What's next?

- [Headless Coding Agent API](/docs/local-agent.md): choose an agent, model, and effort, and see how calls are sandboxed
- [Interactive Coding Agent Session](/docs/local-agent/sessions.md): hand a task to the user's coding agent in a terminal
- [LLM Provider APIs](/docs/direct.md): providers, model aliases, and edge runtimes
- [Examples](/docs/examples.md): complete, real-world examples
