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.
# 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-localScaffold 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.
# 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 typesDefine 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.
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.
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.
Models
anthropic, openai, openrouter, vercel-gateway, ollama, groq. Install one package per provider you use; route per agent with createModelFactory.
Adapter docs →Storage
storage-local (LibSQL bootstrap) for dev; storage-supabase (Postgres + Realtime + credit ledger) for production.
Adapter docs →Runners
runner-nextjs streams interactively; runner-background parks HITL turns on a durable waitpoint and resumes across process death.
Adapter docs →Auth & telemetry
auth-supabase and auth-auth0 resolve identity server-side; telemetry-otel and telemetry-langfuse export gen_ai.* spans.
Adapter docs →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.
// 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.
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>
);
}Where to go next
Dive into the module reference for the full API surface, or read the architecture story behind the framework.