---
name: nightowls-adopt
description: Use when adding Night Owls agents to an existing TypeScript or Next.js app, or when asked to install @nightowlsdev packages, define a swarm, mount a chat route, or wire the runner.
license: MIT
metadata:
  source: https://nightowls.dev/skills/nightowls-adopt.md
  verified: "2026-07-28"
---

# Adopting Night Owls

Night Owls adds governed multi-agent chat to an app you already have. You keep your framework,
your auth, and your database. It is not a platform you migrate onto.

## Install

The engine plus one runner, one storage adapter, one model adapter, and the UI:

```bash
pnpm add @nightowlsdev/core@^0.21.0 @nightowlsdev/runner-nextjs@^3.5.0 \
         @nightowlsdev/storage-supabase@^2.8.0 @nightowlsdev/react@^2.11.0 ai
```

`ai` (v7) is a required peer of the runner. `next >= 15` is optional and types-only.

## Define a swarm

```ts
// lib/swarm.ts
import { defineAgent, defineTool, defineSkill, defineSwarm } from "@nightowlsdev/core";
import { z } from "zod";

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}` }),
});

const coordinator = defineAgent({
  slug: "coordinator",
  role: "orchestrator",
  personality: "A concise assistant.",
  capabilities: [],
  skills: [defineSkill(greet)],
  delegates: [],
  modelId: "openai/gpt-5.5",
});

export const { engine } = defineSwarm({
  agents: [coordinator],
  models: { allow: ["openai/gpt-5.5"] },
});
```

Field names that are easy to get wrong: an agent takes `personality` (not `instructions`) and
`modelId` (not `model`), and `role` is a union, not free text.

## Mount the routes

```ts
// lib/runner.ts
import { createNextjsRunner } from "@nightowlsdev/runner-nextjs";
export const runner = createNextjsRunner({ engine, auth, storage });
```

```ts
// app/api/swarm/chat/route.ts
export const runtime = "nodejs";
export const maxDuration = 300;
export const { POST } = runner.chatRoute();
```

Also mount `resumeRoute()` and `eventsRoute()`. Resume is required for human-in-the-loop: without
it an agent that asks a question can never be answered.

## Mount the UI

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

export function Chat({ threadId }: { threadId: string }) {
  return (
    <SwarmProvider theme="ink">
      <SwarmChat agentSlug="coordinator" threadId={threadId} />
    </SwarmProvider>
  );
}
```

## Attachments

`<SwarmChat>` is text-only until you wire one prop. `onUpload={(file) => Promise<AttachmentRef>}` puts the
bytes in your own bucket and returns a ref; the paperclip appears only when that prop is wired *and* the engine
declares a `multimodal` class. The bytes never cross the framework's wire — a ref carries an opaque id plus
display hints, and a ref with a `url`/`path`/`key` field is rejected server-side. Turning refs back into
something a model can read is the other half, `SwarmConfig.resolveAttachment`. Two v1 limits worth knowing
before you promise them: on core/mastra a tool cannot pause for human approval inside a turn that carries
attachments (it fails with `attachments-suspend-unsupported`; `@nightowlsdev/engine-ai-sdk` supports the full
cycle), and a delegate receives text, never the file. Full recipe: https://nightowls.dev/docs/attachments

## What will surprise you

Identity is server-side only. The runner builds every `SwarmContext` from your `AuthProvider`;
`tenantId` and `userId` in a request body are ignored, and `runId` is server-minted. Do not try to
pass identity from the browser, it will not work and that is deliberate.

Every thread-scoped route checks participation before returning data. A same-tenant non-participant
with a guessed thread id gets a 403.

`pnpm test` in this framework does not typecheck. If you are verifying a change, run `typecheck`
separately.

## Next

For the governance model that makes this different from calling a model directly, use the
`nightowls-governance` skill. For the admin UI, use `nightowls-ai-studio`.
