# {{PROJECT_NAME}} — Lobu project guide

This file is auto-loaded by your coding agent and is the source of truth for how
to build and run this Lobu project. Follow it — you do not need any separately
installed skill or plugin.

## What Lobu is

An open-source, event-sourced backend for AI agents:

- **Data connectors** pull from external sources (GitHub, a website, a webhook,
  a CSV) via **feeds** that may use checkpoints to collect incrementally and emit
  **events**. Chat integrations such as Slack may instead deliver incoming
  messages directly and expose no feed. When onboarding a user, explain the
  selected connector's actual model in plain terms before writing config.
- **Memory** turns events into a structured, append-only knowledge graph of typed
  **entities** and **relationships**.
- **Agents** react to messages in real time and answer over chat platforms, HTTP,
  and MCP.
- **Automations** subscribe an agent to connector events, declared event outputs
  from other Automations, schedules, or an explicit call (a PR review, Slack
  reply, daily digest, etc.). Their instructions come from prompts and the
  agent's skills, referenced by name.

`lobu run` boots the whole thing (gateway + worker + memory) as one Node process
on http://localhost:{{GATEWAY_PORT}}.

## Onboarding the user

Ground the project in the user's real business before designing schemas or
Automations. Ask one question at a time: the company or product and what it does,
its public domain (optional for a personal project), the agent's goal and users,
what it may do autonomously, what it should remember, its first data source, and
where people should interact with it. Offer the Web UI, API/SDK, MCP, and supported
chat platforms without requiring Slack or another chat platform.

Ask for permission to research the public website and other public sources. If
the user consents, research the business, cite the public sources, and summarize
its product, customers, workflows, and useful signals. Propose one to three
entity types, their relationships, the initial connector, and one or two useful
Automations. Play the plan back and wait for confirmation before scaffolding it.
An email supplied for Lobu login identifies the user; do not infer their company
or public domain from a personal email address.

## Prerequisites

Before `lobu run`, the host needs Node 22-24 or 26+ (only Node 25 is unsupported —
`isolated-vm` skips that EOL non-LTS line) and one LLM provider credential in
`.env`.
Postgres is built in: `lobu run`
starts an embedded PostgreSQL (with `pgvector`); set `DATABASE_URL` only to use an
external Postgres instead. If a prerequisite is missing, stop and help the user
install it — don't work around it.

## Layout

- `lobu.config.ts` — the entire project as TypeScript. Source of truth; the only
  file `lobu apply` reads.
- `agents/<id>/IDENTITY.md`, `SOUL.md`, `USER.md` — agent persona / semantics /
  user context.
- `connectors/*.connector.ts` — custom data sources (only when no bundled
  connector fits).
- `*.reaction.ts` — code an Automation runs after extraction (post to Slack, update
  an entity).
- `skills/<name>/SKILL.md` — reusable capability bundles for the agent.
- `.env` — secrets (`DATABASE_URL`, encryption keys, provider keys, OAuth creds).
  Never open or read this file and never request permission to do so. Inspect
  declared `secret("ENV_NAME")` placeholders and `.env.example` when present.
  Never commit, print, copy, or invent secret values.

## Config API

Author everything in `lobu.config.ts` with helpers from `@lobu/cli/config`:
`defineConfig`, `defineAgent`, `defineEntityType`, `defineRelationshipType`,
`defineAutomation`, `defineSkill`, `defineConnection`, `defineAuthProfile`,
`on`, `every`, `context`, `secret`, plus `connectorFromFile`,
`reactionFromFile`, `skillFromFile`.

Read the complete, working reference before editing:
https://github.com/lobu-ai/lobu/blob/main/examples/lobu-crm/lobu.config.ts

### Agent

```ts
const agent = defineAgent({
  id: "{{PROJECT_NAME}}",
  // dir defaults to ./agents/<id> (where init scaffolds IDENTITY.md / SOUL.md / USER.md)
  name: "{{PROJECT_NAME}}",
  description: "<one sentence: what it does>",
  providers: [
    // init prewrites this from --provider; canonical ids include `claude`, `openai`.
    { id: "claude", model: "claude-sonnet-4-20250514", key: secret("ANTHROPIC_API_KEY") },
  ],
  // skills: [skillFromFile("./skills/<name>")],
  // network: { allowed: ["api.example.com"] }, // egress allowlist
});
```

### Chat connections (where people talk to agents)

Declare chat integrations at the project level with `defineConnection`. A linked
DM or channel is bound to an agent by a message Automation. BYO chat `config` values
may use `secret(...)` refs resolved from `.env`.

For `slack` and `telegram` you do not need a bot token to start: use
`credentialMode: "hosted"` for the hosted Lobu bot. `lobu run` then prints a
short-lived `/lobu link <code>` you redeem by DMing the bot. This requires a
cloud login and the agent applied to that cloud workspace: the link targets
the cloud agent, not the separate local agent. Hosted chat does not create a
local Slack data connection or prove local Slack search works. Use
`credentialMode: "byo"` when you run your own app:

```ts
const hostedSlack = defineConnection({
  slug: "hosted-slack",
  connector: "slack",
  credentialMode: "hosted",
  surfaces: ["dm", "channel"],
});

const ownSlack = defineConnection({
  slug: "own-slack",
  connector: "slack",
  credentialMode: "byo",
  config: {
    botToken: secret("SLACK_BOT_TOKEN"),
    signingSecret: secret("SLACK_SIGNING_SECRET"),
  },
});

export default defineConfig({
  agents: [agent],
  connections: [hostedSlack], // Choose hostedSlack or ownSlack.
});
```

When the user chooses Slack, ask which onboarding route they want: join the
hosted Lobu workspace for a quick start, install the Lobu app into an existing
Slack workspace, or bring their own Slack app. Also ask whether the first bound
surface should be their DM with Lobu, a named channel, or both, and whether
people knowledge means conversation participants, bound-channel members, or the
full workspace directory. Use the exact join, install, and link instructions
printed by `lobu run`. A printed `/lobu link <code>` is one-time: redeem it in
the first target DM or channel. In an installed workspace whose installer is
linked to Lobu, bind each additional surface with `/lobu link <agent-id>`;
otherwise generate a fresh code for that surface. Mention the bot in a channel
test. Verify the App Home separately. A working DM does not prove a channel
binding, and neither proves the App Home rendered.

Lobu CLI email login and Slack workspace installation are separate authorization
steps. The email link authorizes the Lobu CLI session. Installing the managed
Lobu Slack app requires the user or a workspace admin to approve Slack's OAuth
screen; never describe that Slack consent as an email approval. If Slack's
catalog entry has no `detail.feeds_schema`, do not invent or trigger a Slack
feed. Prove its inbound message events through the selected DM and channel
surfaces instead.

Clarify what "knows the people in Slack" means. Installation lets Lobu resolve
senders and maintain membership ACLs for bound channels; it does not by itself
prove that every workspace member's human-readable profile is available to the
agent. Ask whether the user needs only conversation participants, bound-channel
members, or the full workspace directory. Prove the selected scope by querying
the resulting people/member entities and asking the agent a roster question. If
names or non-participants are missing, report that product gap instead of
inventing a directory.

### Entity types (what it remembers)

```ts
const ticket = defineEntityType({
  key: "ticket",
  name: "Ticket",
  required: ["subject"],
  properties: {
    subject: { type: "string", "x-table-label": "Subject", "x-table-column": true },
    status: { type: "string", enum: ["open", "closed"], "x-table-label": "Status", "x-table-column": true },
  },
});
```

Each property is a JSON Schema fragment. `"x-table-column": true` surfaces it as a
column in the admin UI.

### Connection (a bundled connector — preferred)

```ts
// A GitHub connection needs BOTH an account profile (the interactive login) AND
// an oauth_app profile (your OAuth app's client creds) — `lobu run` aborts apply
// with "Select or create an OAuth app profile before creating the connection."
// if the app profile is missing.
const ghAccountAuth = defineAuthProfile({ slug: "gh-account", connector: "github", authKind: "oauth_account", name: "GitHub" });

const ghAppAuth = defineAuthProfile({
  slug: "gh-app",
  connector: "github",
  authKind: "oauth_app",
  name: "GitHub OAuth App",
  credentials: {
    GITHUB_CLIENT_ID: secret("GITHUB_CLIENT_ID"),
    GITHUB_CLIENT_SECRET: secret("GITHUB_CLIENT_SECRET"),
  },
});

const gh = defineConnection({
  slug: "github-main",
  connector: "github",
  name: "GitHub",
  authProfile: ghAccountAuth, // interactive login profile
  appAuthProfile: ghAppAuth, // your OAuth app's creds — required to create the connection
  // declare each profile as a const, then list them all in defineConfig({ authProfiles })
  // Connector sync settings (what to fetch) go on the FEED's `config`, not the
  // connection's — the server stores them on feeds and rejects feed-scoped keys
  // on the connection. The connection's own `config` is only for the rare
  // connection-scoped option a connector declares.
  feeds: [{ feed: "issues", name: "Issues", schedule: "0 */6 * * *", config: { repo_owner: "you", repo_name: "repo" } }],
});
```

**Bundled connector keys** — use the exact key; several are dotted (Gmail is
`google.gmail`, not `gmail` or `google_gmail`): `github`, `slack`, `linear`, `jira`, `google.gmail`,
`google.calendar`, `google.drive`, `microsoft.outlook`, `discord`, `teams`, `telegram`, `webhook`,
`whatsapp`, `gchat`, `rss`, `postgres`,
`hackernews`, `market.quotes`, `reddit`, `x`, `youtube`, `producthunt`.

Device connectors are not in that list: they are advertised by the device at
poll time and appear after it grants permission. `os.shell` is one of them —
a Mac advertises it through Owletto, a headless server through the
connector-worker daemon — so it exists only where a device offers it.

Each connector defines its own `config` keys and feed keys — don't guess them.
Read the installed catalog schemas before authoring config. Validate the authored
project and inspect catalog, connection, and paired-device metadata. Browser
actions use the paired extension. After the user approves provider-data access,
use a server-side feed dry-run.

Discover live capabilities before writing connector config. Call
`client.connections.setupOptions({ connector_key })` in the selected runtime
before requesting OAuth app credentials. It returns verified managed OAuth,
hosted cloud chat, and local choices with their actual next steps. Browser setup
choices cover configured install flows; Telegram uses the link-code flow above.
`cloud_status: "unavailable"` means discovery failed; retry before concluding
that no managed option exists. Offers are not connected accounts. In an
authenticated cloud context, use ClientSDK through Lobu MCP or
`lobu memory exec`. Call `client.organizations.list()` to find organizations
that offer managed connections. On account-scoped `/mcp`, first select an
authorized home workspace with `const home = await client.org(homeSlug)` and use
that client for `home.connections.connectManaged`. The public provider
organization belongs in `managed_by_org`; it need not be selectable by the
current MCP authorization. Run this against the cloud context, not the embedded
runtime. If a matching live offer exists, explain it and, after the user
consents, call
`client.connections.connectManaged({ managed_by_org, connector_key })`
before asking for OAuth client credentials. Complete the returned authorization, then,
if this project was not already generated from that organization, run its
`local_bootstrap_command` from the parent directory where the new project should
be created; use `--here` only after entering an empty directory. Do not create a
blank Lobu scaffold first or repeat the bootstrap when the connection already
has `managedBy` config. Ask for bring-your-own OAuth credentials only when no
suitable managed offer is live or the user explicitly prefers their own app.
Public offer metadata may be discoverable before login, but connecting always
requires the user to log in and consent; knowing their email does not grant
access. In a headless session, confirm the address before running
`lobu login --email <address>`: Lobu emails an approval link, and the approver
must sign in with that same address. After a successful login, an empty
membership list is a tenancy/discovery result, not proof that authentication
failed. Discover public organizations with `client.organizations.list()` and
explain the result. Never revoke or repeat login with `--force` unless the user
separately approves that re-authentication.

Call `client.catalog.listInstalled({ kinds: ["connectors"] })`; if the connector
is not installed, call `client.catalog.listCatalog({ kinds: ["connectors"] })`.
Inspect the selected entry's `detail.auth_schema`, `detail.options_schema`,
`detail.feeds_schema`, and `detail.automation_events`. A connector's feed
`feeds_schema.<feed>.eventKinds` are the default Automation event trigger catalog:
each declared kind is a subscribable `event_type` (the kind slug). A feed's first
successful non-dry sync establishes its baseline without activation; later
inserts of a matching kind activate subscribers. A connector that declares
explicit `automationEvents` (persisted in the catalog as `automation_events`) wins
over that derivation for the trigger picker; keys that differ from the feed's
`eventKinds` fire only when the connector attaches matching `automation_signals`
to the events it emits. If there is no matching Automation event, use a schedule
plus a bounded named source, validate the query against the running local
schema, and read event text from `payload_text`, for example:

```ts
sources: {
  recentMessages:
    "SELECT id, title, payload_text, metadata, occurred_at FROM events WHERE connector_key = 'google.gmail' ORDER BY occurred_at DESC LIMIT 200",
},
```

If the source has no bundled connector, write `connectors/<name>.connector.ts`
(model it on `examples/lobu-crm/npm-downloads.connector.ts` in the repo) and
reference it with `connectorFromFile("./connectors/<name>.connector.ts")`.

### Auth profiles & secrets

Integrations authenticate via `defineAuthProfile` (`oauth_account` = the user logs
in interactively; `oauth_app` = your own OAuth app, creds passed as `secret(...)`).
Every credential is a `secret("ENV_NAME")` placeholder resolved from `.env` — never
paste a real key into the config, and never fabricate one. If a secret is missing,
ask the user to add it to `.env`.

### Automation (event from a connector or workspace, schedule, or manual) + outputs

An Automation's instructions come from the agent's skill library: list skill names
in `skills` and `lobu apply` compiles their bodies (in order) into the frozen
instructions for that Automation version. Schedule triggers, connector events
with `execution: "window"`, workspace events with their default window
execution, and Automations with no triggers (manual) require a prompt, a skill, or
both. A connector event with `execution: "turn"` (chat or another connector signal) may omit
both — the incoming connector signal is the content and a built-in default
applies.

```ts
const digestSkill = defineSkill({
  name: "daily-digest",
  content: "Summarize yesterday's tickets by root cause and post to Slack.",
});
// … add it to the agent: defineAgent({ …, skills: [digestSkill] })

const digest = defineAutomation({
  agent,
  slug: "daily-digest",
  name: "Daily digest",
  triggers: [
    every("0 9 * * *", { active_run: "coalesce", skip_if_unchanged: true }),
    // Or react to a connector event:
    // on("github", "pull_request.created", { connection: gh }),
    // Or to a declared event output from another Automation:
    // { kind: "event", source: "workspace", event_types: ["observation"], match: { namespace: "account-risk" } },
  ],
  skills: ["daily-digest"],
  outputs: {
    tickets: { entity: ticket, key: ["id"] },
    alerts: { event: "observation" },
  },
  // reaction: reactionFromFile<typeof reactionMod>("./daily-digest.reaction.ts"),
});
```

Declared outputs are persisted whether or not a `reaction` is present, and the
complete result remains on its run. A `reaction` lets the Automation act (post a
message, call connectors, or request a governed merge) and can consume those
persisted drafts. Omit `outputs` for a run-result-only or reaction-only Automation.
Only newly persisted declared event outputs activate triggers with
`source: "workspace"`. Ordinary knowledge saves and connector-ingested events
do not activate workspace-source triggers; connector events activate only
matching connector-source triggers. SQL sources are reads rather than
subscriptions. Wrap reference-only SQL with `context("SELECT …")` so its rows
reach the agent without being linked into the window's event set. The downstream
run reads the exact event pointers plus its
authored sources under the owning agent's ACL. See the complete primitive and limit model at
https://github.com/lobu-ai/lobu/blob/main/docs/AUTOMATIONS.md.
For an entity output, `key: ["a", "b"]` is one exact composite identity tuple,
not two alternative keys. Use one to four durable fields; every row must include
every field as a non-blank string (at most 256 UTF-8 bytes), safe integer, or
boolean. Field order, the Automation, and the output name are part of identity, so
do not key on editable display labels.
Event output rows use the standard draft fields: required `content`,
plus optional `title`, `metadata`, `author`, `source_url`, `occurred_at`,
`parent_event_id`, `payload_type`, and `idempotency_key`. Use
`idempotency_key` when the same real-world source must emit at most once across
runs. Omit `triggers` for a manual-only Automation.

### Wire it together

```ts
export default defineConfig({
  org: "<slug>",
  orgName: "<Display name>",
  agents: [agent],
  entities: [ticket],
  connections: [gh],
  authProfiles: [ghAccountAuth, ghAppAuth],
  automations: [digest],
});
```

## Commands

```bash
npx @lobu/cli@latest run        # boot locally on http://localhost:{{GATEWAY_PORT}}
npx @lobu/cli@latest validate   # check the config before running
npx @lobu/cli@latest login      # auth the CLI (device-code flow)
npx @lobu/cli@latest apply      # push this config to a Lobu org (cloud or self-host)
```

## Completion gate

Run `lobu validate`, `lobu run`, and every project-local check from the generated
project directory. Before each phase, verify that `pwd` contains the intended
`lobu.config.ts`; never run the project from its parent directory.

Prove the selected local path before offering deployment: validate, boot, verify
health and the Web UI, send a harmless direct message without tools, check the
connector without reading provider data, and ask before accessing real provider
data. After consent, dry-run each selected sync-capable feed before a real sync and
run each selected Automation once. Skip feed triggering for chat integrations and
other connectors that expose no sync-capable feed. Show each completed run and any
declared result event or entity. Use the authoritative `view_url` returned by
Lobu for resources that provide one; never invent or reconstruct URLs. Keep
external actions in approval mode unless the user explicitly approves them.

With the embedded database default, `lobu run` creates and selects the `local`
context and auto-applies the project. An external `DATABASE_URL` does neither;
authenticate and apply to that target explicitly. Lobu Cloud is also a separate
target. Because `lobu apply` has no context flag, run `lobu context list`, then
`lobu context use <cloud-name>` to select an explicit cloud context for
deployment. Run `lobu apply --dry-run`, show the target organization and exact
plan, and wait for confirmation before the real apply. Afterwards, present only
the working access paths the user selected: Web UI, direct agent chat, API/SDK,
MCP, and configured chat platforms. Do not call onboarding complete until every
selected path passes end to end. If authorization, a provider, deployment, or a
rate limit blocks the proof, report that exact blocker and keep the result marked
blocked.

## Conventions

- **Never fabricate credentials, tokens, or API keys.** Ask the user; store them in
  `.env` and reference them with `secret(...)`.
- **Pause at every real decision and ask the user** — don't guess what to build.
- **`events` is append-only** — never delete rows; supersede/tombstone instead.
- **Search before create** to avoid duplicate entities.
- Run `lobu validate` after editing the config; boot with `lobu run` and verify a
  real message plus the resulting memory event before calling it done.

@TESTING.md
