# Quickstart

Source: https://docs.havilo.ai/reference/ai-engine/quickstart

This quickstart creates a simple query run, waits for it to finish, and fetches the resulting table artifact.

## 1. Create a Run

```bash
curl -X POST https://havilo.ai/api/v1/runs \
  -H "Authorization: Bearer $HAVILO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "input_type": "query",
    "query": "list 3 climate-tech startups in Europe",
    "config": {
      "strict_mode": true
    }
  }'
```

Successful responses return `202 Accepted`:

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "object": "run",
  "status": "pending",
  "api_version": "2026-04-22",
  "created_at": 1714500000000,
  "workspace_id": "8b3d0c7a-4ef5-4f11-9e8a-8d3b08c29c10",
  "resumable": false,
  "request_id": "req_01hxbexample",
  "input": {
    "input_type": "query",
    "query": "list 3 climate-tech startups in Europe",
    "config": {
      "strict_mode": true
    }
  },
  "config": {
    "strict_mode": true
  }
}
```

## 2. Poll the Run

```bash
curl https://havilo.ai/api/v1/runs/550e8400-e29b-41d4-a716-446655440000 \
  -H "Authorization: Bearer $HAVILO_API_KEY"
```

When the run succeeds, the envelope includes completed steps and artifact metadata:

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "object": "run",
  "status": "succeeded",
  "api_version": "2026-04-22",
  "created_at": 1714500000000,
  "completed_at": 1714500018000,
  "workspace_id": "8b3d0c7a-4ef5-4f11-9e8a-8d3b08c29c10",
  "input": {
    "input_type": "query",
    "query": "list 3 climate-tech startups in Europe",
    "config": {
      "strict_mode": true
    }
  },
  "config": {
    "strict_mode": true
  },
  "steps": [
    {
      "index": 0,
      "kind": "fetch",
      "status": "completed",
      "title": "Find climate-tech startups",
      "duration_ms": 1870,
      "items_out": 3
    },
    {
      "index": 1,
      "kind": "reconstitute",
      "status": "completed",
      "title": "Format results",
      "duration_ms": 950
    }
  ],
  "message": "Here are 3 climate-tech startups.",
  "artifacts": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440001",
      "kind": "table",
      "title": "Climate-Tech Startups",
      "row_count": 3
    }
  ],
  "warnings": [],
  "errors": [],
  "usage": {
    "calls": 4,
    "unpriced_calls": 0,
    "input_tokens": 12000,
    "cached_input_tokens": 6000,
    "output_tokens": 1800,
    "thinking_tokens": 300,
    "cost_usd": 0.02
  },
  "resumable": false,
  "request_id": "req_01hxbexample"
}
```

## 3. Fetch the Artifact

```bash
curl https://havilo.ai/api/v1/artifacts/550e8400-e29b-41d4-a716-446655440001 \
  -H "Authorization: Bearer $HAVILO_API_KEY"
```

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440001",
  "object": "artifact",
  "kind": "table",
  "title": "Climate-Tech Startups",
  "run_id": "550e8400-e29b-41d4-a716-446655440000",
  "created_at": 1714500018000,
  "row_count": 3,
  "columns": ["Name", "Description", "Website", "Location"],
  "rows": [
    {
      "Name": "Example Startup",
      "Description": "A short company description.",
      "Website": "https://example.com",
      "Location": "San Francisco, CA"
    }
  ]
}
```

## TypeScript Example

A complete client with a stable idempotency key, terminal-state polling with exponential backoff, and structured error extraction.

```typescript
const BASE = 'https://havilo.ai/api/v1'
const TERMINAL = new Set(['succeeded', 'failed', 'canceled'])

type Run = {
  id: string
  status: 'pending' | 'running' | 'paused' | 'succeeded' | 'failed' | 'canceled'
  message?: string
  artifacts?: Array<{ id: string; kind: string; title: string }>
  errors?: Array<{ code: string; message: string }>
}

async function createRun(apiKey: string, query: string, idempotencyKey: string): Promise<Run> {
  const response = await fetch(`${BASE}/runs`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${apiKey}`,
      'Content-Type': 'application/json',
      // Reuse this value for every retry of the same logical create request.
      'Idempotency-Key': idempotencyKey,
    },
    body: JSON.stringify({
      input_type: 'query',
      query,
      config: { strict_mode: true },
    }),
  })

  if (!response.ok) {
    const body = await response.json().catch(() => null)
    const code = body?.error?.code ?? `http_${response.status}`
    const message = body?.error?.message ?? response.statusText
    throw new Error(`${code}: ${message}`)
  }

  return response.json()
}

async function pollRun(apiKey: string, runId: string): Promise<Run> {
  let delayMs = 1000
  while (true) {
    await new Promise((r) => setTimeout(r, delayMs))

    const response = await fetch(`${BASE}/runs/${runId}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    })
    if (!response.ok) throw new Error(`poll failed: ${response.status}`)

    const run: Run = await response.json()
    if (TERMINAL.has(run.status)) return run

    delayMs = Math.min(delayMs * 1.5, 5000) // cap at 5s
  }
}

async function getArtifact<T = unknown>(apiKey: string, artifactId: string): Promise<T> {
  const response = await fetch(`${BASE}/artifacts/${artifactId}`, {
    headers: { Authorization: `Bearer ${apiKey}` },
  })
  if (!response.ok) throw new Error(`artifact fetch failed: ${response.status}`)
  return response.json()
}

async function main() {
  const apiKey = process.env.HAVILO_API_KEY!
  const query = 'list 3 climate-tech startups in Europe'
  const idempotencyKey = `query-${crypto.randomUUID()}`
  const created = await createRun(apiKey, query, idempotencyKey)
  const run = await pollRun(apiKey, created.id)

  if (run.status !== 'succeeded') {
    const first = run.errors?.[0]
    throw new Error(first ? `${first.code}: ${first.message}` : `run ${run.status}`)
  }

  console.log(run.message)
  for (const artifact of run.artifacts ?? []) {
    const body = await getArtifact(apiKey, artifact.id)
    console.log(artifact.kind, body)
  }
}
```
