Skip to content
Night Owls.dev
Jump to a page
Getting started

From zero to a running swarm

Install the Night Owls framework, define one agent, and run it locally on just a model key, then wire storage, auth, runner, and UI adapters when you're ready.

Install

Add the engine core and a single model provider. The local LibSQL store lets you run a swarm with nothing but a model key, no database required yet.

terminal
# The engine core + a model provider (add more providers later; they compose)
pnpm add @nightowlsdev/core @nightowlsdev/provider-anthropic

# Local LibSQL snapshot store, run on just a model key, before Supabase
pnpm add @nightowlsdev/storage-local

Scaffold with the CLI

The owl CLI bootstraps a host: it writes nightowls.config.ts, merges adapter env vars, scaffolds files, and ejects each adapter's migrations into supabase/migrations/. Night Owls never runs DDL, you apply migrations with your own tooling.

terminal
# Scaffold nightowls.config.ts + .env.example and pick your adapters
npx owl init

# Add an adapter later (merges env, scaffolds files, ejects its migrations)
npx owl install storage-supabase

# List installed plugins; generate DB types
npx owl plugins
npx owl db types

Define your first agent

Declare a typed tool with defineTool, bundle it into a skill, give an agent a role and personality, then compose a runnable swarm. The swarm reads ANTHROPIC_API_KEY from the environment by default.

agent.ts
import { z } from "zod";
import {
  defineTool,
  defineSkill,
  defineAgent,
  defineSwarm,
  InMemoryStorage,
} from "@nightowlsdev/core";
import { anthropicModels } from "@nightowlsdev/provider-anthropic";
import { createMastraLibsqlStore } from "@nightowlsdev/storage-local";

// 1. A typed tool, the smallest unit of capability.
const greet = defineTool({
  name: "greet",
  description: "Greet a person by name.",
  inputSchema: z.object({ name: z.string() }),
  outputSchema: z.object({ greeting: z.string() }),
  execute: async ({ name }) => ({ greeting: `Hello, ${name}!` }),
});

// 2. An agent: a role, a personality, and a set of skills.
const helper = defineAgent({
  slug: "helper",
  role: "orchestrator",
  personality: "A friendly assistant who greets people warmly.",
  skills: [defineSkill(greet)],
  modelId: "claude-sonnet-4-5",
});

// 3. Compose a runnable swarm. This IS the whole minimal config — every field
//    below is required by defineSwarm, and each has a zero-infra dev value:
const swarm = defineSwarm({
  agents: [helper],
  // In-memory store: run locally with no database (swap for storage-supabase in prod).
  storage: new InMemoryStorage(),
  // The per-tenant model allow-list every resolved model must pass.
  models: { allow: ["claude-sonnet-4-5"] },
  // Reads ANTHROPIC_API_KEY from the environment by default.
  modelFactory: anthropicModels(),
  // A simple per-run budget: at most 20 steps and $1 of model spend.
  cost: { maxSteps: 20, maxCostUsd: 1 },
  // Durable snapshot store for human-in-the-loop resume. :memory: for a single
  // session; an absolute file: url to resume across process restarts.
  mastraStore: createMastraLibsqlStore({ url: ":memory:" }),
});

Run it

Construct a SwarmEngine and drain the typed SwarmEvent stream. Each run emits assistant messages, delegation and handoffs, tool activity, status, and human-in-the-loop asks.

run.ts
import { SwarmEngine } from "@nightowlsdev/core";

const engine = new SwarmEngine(swarm);

// Drain the typed SwarmEvent stream from a single run.
for await (const event of engine.run(ctx, { message: "Say hi to Ada." })) {
  console.log(event.type, event);
}

Wire adapters

Everything around the engine is a swappable adapter. Pick a model provider, a storage backend, an interactive or durable runner, an auth provider, and a telemetry exporter, install each with owl install.

Serve a run from Next.js

The Next.js runner returns three App Router route factories. Identity is resolved server-side, a forged tenant in the request body is ignored.

route.ts
// app/api/swarm/chat/route.ts
import { createNextjsRunner } from "@nightowlsdev/runner-nextjs";
import { supabaseAuth } from "@nightowlsdev/auth-supabase";
import { createSupabaseStorage } from "@nightowlsdev/storage-supabase";

const auth = supabaseAuth({
  url: process.env.SUPABASE_URL!,
  anonKey: process.env.SUPABASE_ANON_KEY!,
});
const storage = createSupabaseStorage({
  url: process.env.SUPABASE_URL!,
  secretKey: process.env.SUPABASE_SECRET_KEY!,
});

const runner = createNextjsRunner({ swarm, auth, storage });

// Streams an AI SDK v7 UI Message Stream to the browser.
export const POST = runner.chatRoute();

Render it in React

Wrap your tree in SwarmProvider, import the precompiled styles, and drop in SwarmChat, no Tailwind build needed in the host.

room.tsx
import { SwarmProvider, SwarmChat } from "@nightowlsdev/react";
import "@nightowlsdev/react/styles.css";

export function Room() {
  return (
    <SwarmProvider mode="dark" theme={{ "--owl-accent": "#E8A33D" }}>
      <SwarmChat agentSlug="helper" threadId="thread-1" />
    </SwarmProvider>
  );
}