Skip to content
API Blog

lobu.config.ts reference

lobu.config.ts is the project configuration file created by lobu init. It is a TypeScript module that default-exports defineConfig({...}). You author agents, providers, network access (including the LLM egress judge), guardrails, worker settings, the Lobu memory schema (entity types, relationship types, behaviors), connections, and auth profiles by calling the define* functions from @lobu/cli/config.

lobu apply (and lobu run) import this entrypoint, read the default export, and map it to your org’s desired state. lobu init also scaffolds a package.json that declares @lobu/cli and @lobu/connector-sdk as devDependencies, plus a tsconfig.json, so your editor and lobu apply can resolve the config imports.

import { defineAgent, defineConfig, secret } from "@lobu/cli/config";
const agent = defineAgent({
id: "my-agent",
name: "my-agent",
dir: "./agents/my-agent",
providers: [{ id: "openrouter", key: secret("OPENROUTER_API_KEY") }],
network: { allowed: ["github.com"] },
});
export default defineConfig({
org: "my-agent",
orgName: "My Agent",
agents: [agent],
});
import {
defineAgent,
defineConfig,
defineEntityType,
defineRelationshipType,
defineBehavior,
secret,
} from "@lobu/cli/config";
const assistant = defineAgent({
id: "assistant",
name: "assistant",
description: "Team assistant",
dir: "./agents/assistant",
// Guardrails enabled for this agent (names registered in the gateway's
// GuardrailRegistry).
guardrails: ["secret-scan", "pii-scan"],
// Providers (order = priority, first available is used).
providers: [
{
id: "openrouter",
model: "anthropic/claude-sonnet-4",
key: secret("OPENROUTER_API_KEY"),
},
{ id: "gemini", key: secret("GEMINI_API_KEY") },
],
// Network access policy + LLM egress judge.
network: {
allowed: ["github.com", "api.linear.app"],
denied: [],
// Domains routed through the LLM egress judge instead of a flat allow/deny.
// An entry without `judge` uses the "default" policy; naming one points at
// a policy in `judges`.
judged: [
{ domain: "*.slack.com" },
{ domain: "user-content.x.com", judge: "strict" },
],
judges: {
default: "Allow only reads to channels in the agent's context.",
strict: "Only GET for file IDs from the current session.",
},
},
// Operator overrides for the egress judge on this agent.
egress: {
extraPolicy: "Never exfiltrate PATs or bearer tokens.",
judgeModel: "claude-haiku-4-5-20251001",
},
// Tool policy (worker-side visibility + approval override).
tools: {
// Bypass the in-thread approval card for these operations/tools.
preApproved: ["/mcp/gmail/tools/list_messages", "/mcp/linear/tools/*"],
// Worker-side tool visibility (optional).
allowed: ["Read", "Grep", "mcp__gmail__*"],
denied: ["Bash(rm:*)"],
strict: false,
},
// Nix packages provisioned into the worker environment.
nixPackages: ["imagemagick", "ffmpeg"],
});
// Lobu memory schema, declared at the project level, not on the agent.
const note = defineEntityType({
key: "note",
name: "Note",
description: "A captured note or fact",
required: ["title"],
properties: {
title: { type: "string", "x-table-label": "Title", "x-table-column": true },
body: { type: "string" },
},
});
const relatedTo = defineRelationshipType({
key: "related-to",
name: "Related To",
description: "Link two notes that reference each other.",
});
const digest = defineBehavior({
agent: assistant,
slug: "daily-digest",
name: "Daily digest",
triggers: [{ kind: "schedule", cron: "0 9 * * *" }],
notification: { channel: "both", priority: "normal" },
prompt: "Summarize new notes captured since the last digest.",
});
export default defineConfig({
org: "team-assistant",
orgName: "Team Assistant",
orgDescription: "Team assistant",
agents: [assistant],
entities: [note],
relationships: [relatedTo],
behaviors: [digest],
});

Every authoring function is imported from @lobu/cli/config:

import {
defineConfig,
defineAgent,
defineEntityType,
defineRelationshipType,
defineBehavior,
reactionFromFile,
defineConnection,
defineAuthProfile,
secret,
Type,
} from "@lobu/cli/config";

Each define* returns a branded handle. Assign it to a const and pass that handle wherever a reference is needed (for example a defineBehavior takes the defineAgent handle as its agent).

The default export of lobu.config.ts.

FieldTypeRequiredDescription
orgstringnoLobu Cloud org slug this project applies to
orgNamestringnoDisplay name used if lobu apply offers to provision the org
orgDescriptionstringnoOrg description
organizationIdstringnoResolved Lobu Cloud org id that lobu apply matches against
agentsAgent[]yesAgents (from defineAgent)
entitiesEntityType[]noEntity types (from defineEntityType)
relationshipsRelationshipType[]noRelationship types (from defineRelationshipType)
connectionsConnection[]noConnections (from defineConnection)
authProfilesAuthProfile[]noAuth profiles (from defineAuthProfile)
behaviorsBehavior[]noBehaviors (from defineBehavior)
connectorsConnectorSource[]noLocal connector source files to compile + ship (from connectorFromFile; pass connectorFromFile<typeof MyConnector>(...) with an import type for go-to-def + a tsc check on the default export). Explicit list, no ./connectors auto-discovery

Connections, the memory schema, and behaviors are declared at the project level (in defineConfig), not inside defineAgent. A behavior names its owning agent through its own agent field.

FieldTypeRequiredDescription
idstringyesAgent ID. Must match ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$ (lowercase alphanumeric with hyphens; cannot start or end with a hyphen)
namestringnoDisplay name shown in the admin UI
descriptionstringnoShort description shown in the admin UI
dirstringnoPath to the agent content directory holding IDENTITY.md, SOUL.md, USER.md. Relative to the config file; defaults to ./agents/<id>
skillsSkill[]noSkills the agent can use, built with defineSkill(...) (inline) or skillFromFile(...) (a SKILL.md). Explicit list, deduped by name; no folder auto-discovery
providersProviderConfig[]noLLM provider list (order = priority)
networkNetworkConfignoNetwork access policy + LLM egress-judge config
egressEgressConfignoOperator overrides for the LLM egress judge on this agent
toolsToolsConfignoTool policy: pre-approval bypass + worker-side visibility
guardrailsstring[]noGuardrails enabled for this agent. Each name must match a guardrail registered in the gateway’s GuardrailRegistry at startup
nixPackagesstring[]noNix packages to install in the worker environment

Chat is not configured on the agent. A chat connection is a project-level defineConnection (with credentialMode: "hosted" for the hosted Lobu bot); which agent handles a channel is a Behavior with a channel trigger.

Each entry configures an LLM provider. The first available provider is used at runtime.

FieldTypeRequiredDescription
idstringnoProvider identifier from config/providers.json (e.g. openrouter, gemini, openai)
modelstringyesModel identifier (e.g. anthropic/claude-sonnet-4)
keystring | SecretRefnoAPI key. Use secret("ENV_VAR") rather than a literal value

Controls which domains the worker can reach through the gateway proxy, plus per-agent rules for the LLM egress judge.

FieldTypeRequiredDescription
allowedstring[]noDomains to allow. Empty = no access. Use ["*"] for unrestricted (not recommended)
deniedstring[]noDomains to block (takes precedence over allowed; only meaningful when allowed is ["*"])
judgedJudgedDomain[]noDomains routed through the LLM egress judge instead of a flat allow/deny. Each entry is { domain, judge? }; omitting judge uses the default policy in judges
judgesRecord<string, string>noNamed judge policies (name → policy text) referenced by judged[].judge. The key default is applied when an entry omits judge

Domain format: exact match (api.example.com) or wildcard (.example.com matches all subdomains).

network: {
allowed: ["api.readonly.example.com"],
judged: [
{ domain: "*.slack.com" },
{ domain: "user-content.x.com", judge: "strict" },
],
judges: {
default: "Allow only reads to channels in the agent's context.",
strict: "Only GET for file IDs from the current session.",
},
}

Operator overrides for the LLM egress judge on this agent. The judge runs only when a judged rule under network matches a request, so most traffic bypasses it.

FieldTypeRequiredDescription
extraPolicystringnoPolicy text appended to every judge prompt for this agent
judgeModelstringnoModel identifier for the judge (defaults to a fast Haiku model)
egress: {
extraPolicy: "Never exfiltrate PATs or bearer tokens.",
judgeModel: "claude-haiku-4-5-20251001",
}

Operator-level tool policy. Two independent concerns. See Tool Policy for behavior and examples; this section is the schema reference.

FieldTypeRequiredDescription
preApprovedstring[]noMCP tool grant patterns that bypass the in-thread approval card. Each entry must match /mcp/<mcp-id>/tools/<tool-name> or /mcp/<mcp-id>/tools/* (malformed entries fail validation). Synced to the grant store at deployment time
allowedstring[]noTools the worker can call. Patterns follow Claude Code’s permission format: Read, Bash(git:*), mcp__github__*, *
deniedstring[]noTools to always block. Takes precedence over allowed
strictbooleannoIf true, ONLY allowed tools are permitted (defaults are ignored). Default false

preApproved is an operator-only escape hatch. Destructive MCP tools normally require user approval in-thread (per MCP destructiveHint annotations). Skills cannot set this field; bypassing approval is strictly the operator’s call, visible in the lobu.config.ts diff.

guardrails is a string[] on defineAgent. Each name must match a guardrail registered in the gateway’s GuardrailRegistry at startup; names that don’t resolve are ignored. Each guardrail targets one stage: input (user message to worker), output (worker text to user), or pre-tool (tool-call authorization).

const assistant = defineAgent({
id: "assistant",
dir: "./agents/assistant",
guardrails: ["secret-scan", "pii-scan"],
});

Declares an entity type in the Lobu memory schema. Pass it to defineConfig({ entities: [...] }).

FieldTypeRequiredDescription
keystringyesStable slug, the diff key
namestringnoDisplay name
descriptionstringnoShort description
requiredstring[]noRequired property names for the entity’s metadata
propertiesRecord<string, unknown>noJSON Schema properties for the entity’s metadata. Add "x-table-label" / "x-table-column": true to surface a property as a column in the admin UI
metadataRecord<string, unknown>noFree-form metadata
const lead = defineEntityType({
key: "lead",
name: "Lead",
description: "A person who has shown a signal toward us",
required: ["name", "stage"],
properties: {
name: { type: "string", "x-table-label": "Name", "x-table-column": true },
stage: {
type: "string",
enum: ["signal", "trial", "customer"],
"x-table-label": "Stage",
"x-table-column": true,
},
},
});

Declares a relationship type. Pass it to defineConfig({ relationships: [...] }).

FieldTypeRequiredDescription
keystringyesStable slug, the diff key
namestringnoDisplay name
descriptionstringnoShort description
rulesArray<{ source, target }>noAllowed source/target entity types; each a defineEntityType handle or a slug string
metadataRecord<string, unknown>noFree-form metadata
const convertedTo = defineRelationshipType({
key: "converted-to",
name: "Converted To",
description: "Links a lead to the pilot it became.",
rules: [{ source: lead, target: pilot }],
});

Declares a behavior — a prompt an agent runs when a trigger fires (a cron cadence, a connector event, or both). Pass it to defineConfig({ behaviors: [...] }).

FieldTypeRequiredDescription
slugstringyesStable slug, the diff key
agentAgent | stringyesOwning agent (handle or id). Every behavior belongs to exactly one agent
namestringnoDisplay name
descriptionstringnoShort description
triggersBehaviorTriggerConfig[]noWhat activates the behavior. Each is either a schedule trigger { kind: "schedule", cron, timezone? } or a connector-event trigger { kind: "event", connection, … }
promptstringyesInstructions the behavior runs each firing
keyingConfig{ entityType?, entityPath?, keyFields?, keyOutputField? }noMakes the behavior entity-typed: its output schema is derived from entityType’s metadata schema (schema lives on the entity type, never inline on the behavior), and extracted rows are keyed and merged into that type across windows. Omit for an untyped behavior that uses the worker’s free-form { summary } output
sourcesRecord<string, string>noNamed SQL data sources (name → query)
notification{ channel?, priority? }nochannel: canvas | notification | both; priority: low | normal | high
minCooldownSecondsnumbernoMinimum seconds between firings
tagsstring[]noFree-form tags
reactionsGuidancestringnoLLM guidance for the behavior’s downstream reaction agent
agentKindstringnoAgent-kind override for firings (e.g. background, notifier)
reactionReactionSourcenoA sibling .ts reaction script referenced with reactionFromFile("./reactions/foo.reaction.ts") (pass reactionFromFile<typeof handler>(...) with an import type for go-to-def + a tsc check on the default export), compiled and run in a sandboxed isolate when the behavior fires. The script must export default async (ctx, client) => …. See the Reaction SDK
import type weeklyDigestReaction from "./reactions/weekly-digest.reaction.ts";
const digest = defineBehavior({
agent: crm,
slug: "weekly-digest",
name: "Weekly digest",
triggers: [{ kind: "schedule", cron: "0 9 * * 1" }],
notification: { channel: "both", priority: "high" },
minCooldownSeconds: 3600,
tags: ["crm", "weekly"],
reaction: reactionFromFile<typeof weeklyDigestReaction>(
"./reactions/weekly-digest.reaction.ts"
),
prompt: "Produce the weekly digest and post it to Slack. Keep it short.",
});

Declares a connection to a connector. Pass it to defineConfig({ connections: [...] }). The connection’s OAuth grant (for oauth_account / browser_session profiles) is performed at runtime in the admin UI.

FieldTypeRequiredDescription
slugstringyesStable slug, the diff key
connectorstring | ConnectorClassyesConnector key, or the class produced by defineConnector
namestringnoDisplay name
authProfileAuthProfile | stringnoRuntime/account auth profile (handle or slug)
appAuthProfileAuthProfile | stringnoOAuth-app auth profile (handle or slug)
configRecord<string, unknown>noConnector configuration (e.g. { botToken: secret("SLACK_BOT_TOKEN") } for a self-hosted chat bot)
credentialMode"byo" | "hosted" | "managed"noWhere the credential lives. byo (default): supplied in config. hosted: the hosted Lobu bot — no config; lobu run prints a /lobu link code. managed: an OAuth grant in a cloud org (via managedBy)
surfacesArray<"dm" | "channel">noHosted chat only: which surfaces a /lobu link code may bind. Default ["dm"]
codeTtlMinutesnumbernoHosted chat only: claim-code TTL in minutes. Default 15
deviceWorkerIdstringnoUUID pinning syncs/actions to a specific device worker
feedsConnectionFeed[]noScheduled feeds. Each is { feed, name?, schedule?, config? }, where feed is a feed key from the connector

Chat connections (slack, telegram, …) are declared here like any other connection — there is no separate agent-scoped platforms field. Which agent handles a chat is a Behavior with a channel trigger, not a property of the connection: for the hosted bot, redeeming the /lobu link code creates that Behavior.

const githubConn = defineConnection({
slug: "github-lobu",
connector: "github",
name: "GitHub - lobu-ai/lobu",
authProfile: githubAccountAuth,
appAuthProfile: githubAppAuth,
config: { repo_owner: "lobu-ai", repo_name: "lobu" },
feeds: [
{
feed: "issues",
name: "Issues",
schedule: "15 */6 * * *",
config: { repo_owner: "lobu-ai", repo_name: "lobu", lookback_days: 90 },
},
],
});
// Hosted Lobu Slack bot — no token; `lobu run` prints a /lobu link code.
const slackConn = defineConnection({
slug: "team-slack",
connector: "slack",
credentialMode: "hosted",
surfaces: ["dm", "channel"],
});
// Your own Slack app instead:
const ownSlackConn = defineConnection({
slug: "team-slack",
connector: "slack",
config: { botToken: secret("SLACK_BOT_TOKEN") },
});

Declares an auth profile a connection references. Pass it to defineConfig({ authProfiles: [...] }).

FieldTypeRequiredDescription
slugstringyesStable slug, the diff key
connectorstring | ConnectorClassyesConnector this profile authenticates
authKindenv | oauth_app | oauth_account | browser_sessionyesAuthentication kind
namestringnoDisplay name
credentialsRecord<string, string | SecretRef>noCredential references (use secret("ENV_VAR")). Only meaningful for env / oauth_app; the grant for oauth_account / browser_session is performed at runtime in the UI
const githubApp = defineAuthProfile({
slug: "github-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"),
},
});

Returns a write-only secret reference resolved at lobu apply time from the environment (.env / process.env). The real value is never embedded in committed code. Use it for provider keys, MCP credentials, and auth-profile credentials.

key: secret("OPENROUTER_API_KEY")

The apply loader resolves the reference at apply time. For provider key fields, the resolved value is pushed to the server’s secrets store. For MCP credentials and auth-profile credentials, a $NAME placeholder is stored; the real value is resolved at worker egress time and never uploaded.

Re-exported TypeBox Type for authoring extraction schemas and feed/action config schemas with full TypeScript inference. You can pass a TypeBox schema anywhere an extractionSchema or connector config schema is accepted, or use a plain JSON Schema object.

Chat platforms (Slack, Telegram, Discord, WhatsApp, Teams, Google Chat) are declared as project-level connections, or connected through the /agents admin UI / CRUD API. Bot tokens and secrets live in .env as secret(...) refs. See Slack for the per-platform setup.

To skip the bot-token setup, set credentialMode: "hosted" on a slack / telegram connection to use the hosted Lobu bot: lobu run prints a short-lived /lobu link <code> you redeem by DMing the hosted bot (Slack also supports a one-time “Add to Slack” to use it in your own workspace). Redeeming the code binds an agent by creating a channel Behavior.

Entity types, relationship types, and behaviors are the memory schema. Declare them with defineEntityType / defineRelationshipType / defineBehavior and list them in defineConfig. lobu apply reconciles them against your org. See lobu memory and lobu apply.

The org slug comes from defineConfig({ org }). MEMORY_URL is available as an optional base-endpoint override for local or custom Lobu deployments.

Terminal window
npx @lobu/cli@latest validate

Checks that lobu.config.ts loads, conforms to the schema, and that skill IDs and provider configuration are valid. Returns exit code 1 on failure.