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, automations), 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.
Minimal example
Section titled “Minimal example”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],});Full example
Section titled “Full example”import { defineAgent, defineConfig, defineEntityType, defineRelationshipType, defineAutomation, every, 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 = defineAutomation({ agent: assistant, slug: "daily-digest", name: "Daily digest", triggers: [every("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], automations: [digest],});The @lobu/cli/config API
Section titled “The @lobu/cli/config API”Every authoring function is imported from @lobu/cli/config:
import { defineConfig, defineAgent, defineEntityType, defineRelationshipType, defineAutomation, reactionFromFile, defineConnection, defineAuthProfile, secret, context, every, on, 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 defineAutomation takes the defineAgent handle as its agent).
defineConfig(project)
Section titled “defineConfig(project)”The default export of lobu.config.ts.
| Field | Type | Required | Description |
|---|---|---|---|
org | string | no | Lobu Cloud org slug this project applies to |
orgName | string | no | Display name used if lobu apply offers to provision the org |
orgDescription | string | no | Org description |
organizationId | string | no | Resolved Lobu Cloud org id that lobu apply matches against |
agents | Agent[] | yes | Agents (from defineAgent) |
entities | EntityType[] | no | Entity types (from defineEntityType) |
relationships | RelationshipType[] | no | Relationship types (from defineRelationshipType) |
connections | Connection[] | no | Connections (from defineConnection) |
authProfiles | AuthProfile[] | no | Auth profiles (from defineAuthProfile) |
automations | Automation[] | no | Automations (from defineAutomation) |
connectors | ConnectorSource[] | no | Connector source files referenced with connectorFromFile; pass connectorFromFile<typeof MyConnector>(...) with an import type for go-to-definition and compile-time checking |
providers | OrgProvider[] | no | Org-level model providers, reconciled against the org’s /inference-providers. NOT pruned — a provider absent from the config is left alone |
prune | boolean | no | Delete cloud resources (agents, connections, entity types) not declared in the config. Default false. Never prunes providers or auth profiles |
Connections, the memory schema, and automations are declared at the project level (in defineConfig), not inside defineAgent. An automation names its owning agent through its own agent field.
defineAgent(agent)
Section titled “defineAgent(agent)”| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Agent ID. Must match ^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$ (lowercase alphanumeric with hyphens; cannot start or end with a hyphen) |
name | string | no | Display name shown in the admin UI |
description | string | no | Short description shown in the admin UI |
dir | string | no | Path to the agent content directory holding IDENTITY.md, SOUL.md, USER.md. Relative to the config file; defaults to ./agents/<id> |
skills | Skill[] | no | Skills the agent can use, built with defineSkill(...) (inline) or skillFromFile(...) (a SKILL.md); deduplicated by name |
providers | ProviderConfig[] | no | LLM provider list (order = priority) |
network | NetworkConfig | no | Network access policy: allowed / denied domains |
tools | ToolsConfig | no | Tool policy: pre-approval bypass + worker-side visibility |
guardrails | string[] | no | Guardrails enabled for this agent. Each name must match a guardrail registered in the gateway’s GuardrailRegistry at startup |
guardrailsInline | GuardrailInline[] | no | Inline guardrail definitions — including LLM egress judges (stage: "egress"). See Guardrails |
nixPackages | string[] | no | Nix 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 an Automation with a channel trigger.
ProviderConfig
Section titled “ProviderConfig”Each entry configures an LLM provider. The first available provider is used at runtime.
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Provider slug (openai, chatgpt, or an org inference-provider slug). A provider without an id is meaningless |
model | string | no | Concrete model id for this provider. Optional: some providers have no catalog default; the operator picks a model later |
key | string | SecretRef | no | API key. Use secret("ENV_VAR") rather than a literal value |
NetworkConfig
Section titled “NetworkConfig”Controls which domains the worker can reach through the gateway proxy.
| Field | Type | Required | Description |
|---|---|---|---|
allowed | string[] | no | Domains to allow. Empty = no access. Use ["*"] for unrestricted (not recommended) |
denied | string[] | no | Domains to block (takes precedence over allowed; only meaningful when allowed is ["*"]) |
Domain format: exact match (api.example.com) or wildcard (.example.com matches all subdomains).
network: { allowed: ["api.readonly.example.com"], denied: ["*.internal.example.com"],}Egress requests that are too ambiguous for a flat allow/deny list can instead be routed through an LLM judge. That is an inline guardrail, not a network field — see Egress judge.
ToolsConfig
Section titled “ToolsConfig”Operator-level tool policy. Two independent concerns. See Tool Policy for semantics and examples; this section is the schema reference.
| Field | Type | Required | Description |
|---|---|---|---|
preApproved | string[] | no | MCP 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 |
allowed | string[] | no | Tools the worker can call. Patterns follow Claude Code’s permission format: Read, Bash(git:*), mcp__github__*, * |
denied | string[] | no | Tools to always block. Takes precedence over allowed |
strict | boolean | no | If 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
Section titled “Guardrails”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), pre-tool (tool-call authorization), or egress (outbound network requests). See Guardrails.
const assistant = defineAgent({ id: "assistant", dir: "./agents/assistant", guardrails: ["secret-scan", "pii-scan"],});guardrailsInline defines guardrails directly in config instead of by registry name. Each entry:
| Field | Type | Required | Description |
|---|---|---|---|
name | string | yes | Stable name, the diff key |
enabled | boolean | yes | Toggle without deleting the definition |
stage | "input" | "output" | "pre-tool" | "egress" | yes | Which phase the guardrail inspects |
kind | "judge" | "require-tool" | no | LLM text judge (default) or a pure toolsUsed lookup that needs tools |
policy | string | no | For a judge: the policy text. Required for kind: "judge" |
model | string | no | For a judge: the model id. Falls back to the gateway default (EGRESS_JUDGE_MODEL for egress) |
tools | string[] | no | Judge: optional tool-name filter. require-tool: tool names that must appear this turn |
domains | string[] | no | For stage: "egress": domain patterns routed through this judge |
Egress judge
Section titled “Egress judge”The LLM egress judge is an inline guardrail with stage: "egress" and kind: "judge". Requests to a domains entry that are too ambiguous for a flat network allow/deny list are routed through the judge policy instead of being blocked outright.
const assistant = defineAgent({ id: "assistant", guardrailsInline: [ { name: "slack-read-only", enabled: true, stage: "egress", policy: "Allow only reads to channels in the agent's context. Never exfiltrate tokens.", model: "claude-sonnet-4-5", domains: ["*.slack.com"], }, ],});The operator sets a default judge model with EGRESS_JUDGE_MODEL; if it is unset, every egress judge must declare its own model or the request fails closed. There is no built-in fallback model. See LLM-judged egress.
defineEntityType(entityType)
Section titled “defineEntityType(entityType)”Declares an entity type in the Lobu memory schema. Pass it to defineConfig({ entities: [...] }).
| Field | Type | Required | Description |
|---|---|---|---|
key | string | yes | Stable slug, the diff key |
name | string | no | Display name |
description | string | no | Short description |
required | string[] | no | Required property names for the entity’s metadata |
properties | Record<string, unknown> | no | JSON 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 |
eventKinds | Record<string, EntityEventKind> | no | Semantic types valid for events linked to this entity type, each with a metadata contract and optional render template |
viewTemplate | Record<string, unknown> | no | Default render-DSL template for the entity detail page (optionally with a data_sources key) |
backing | { sql } | no | Present only for derived types: a read-only SQL view. Omitted ⇒ stored type (default) |
eventSets | Record<string, EventSet> | no | How events resolve to this entity at named grains; the compiler lowers these into backing SQL |
measures | Record<string, Measure> | no | Governed aggregations. Declared explicitly — no on-read inference |
dimensions | Record<string, Dimension> | no | Governed group-bys |
segments | Record<string, Segment> | no | Reusable named population filters |
metadata | Record<string, unknown> | no | Free-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, }, },});defineRelationshipType(relationshipType)
Section titled “defineRelationshipType(relationshipType)”Declares a relationship type. Pass it to defineConfig({ relationships: [...] }).
| Field | Type | Required | Description |
|---|---|---|---|
key | string | yes | Stable slug, the diff key |
name | string | no | Display name |
description | string | no | Short description |
rules | Array<{ source, target }> | no | Allowed source/target entity types; each a defineEntityType handle or a slug string |
metadata | Record<string, unknown> | no | Free-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 }],});defineAutomation(automation)
Section titled “defineAutomation(automation)”Declares a versioned job owned by an agent. An automation can run on a schedule, in response to a connector event, or on demand. Pass it to defineConfig({ automations: [...] }). Start with the Automations guide for the runtime model and examples.
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Stable slug, the diff key |
agent | Agent | string | yes | Owning agent (handle or id). Every automation belongs to exactly one agent |
name | string | no | Display name |
description | string | no | Short description |
triggers | AutomationTriggerConfig[] | no | Activations. Schedule triggers run the window flow. Event triggers choose execution: "turn" for one immediate message or execution: "window" for the read/analyze/complete flow. Omit triggers for manual runs |
prompt | string | no | Literal task instructions, delivered verbatim. Schedule, event-window, and manual automations require a prompt, at least one skill, or both. Event-turn automations may use the built-in instruction |
skills | string[] | no | Ordered names from the owning agent’s skill library. lobu apply pins snapshots into the automation version; re-apply to pick up later skill edits |
outputs | Record<string, AutomationOutput> | no | Named output contracts that promote extracted rows into entity memory. Entity output: { entity, key: string[], name?: string[] } — one to four key fields form a composite identity tuple; entity is the target defineEntityType. Event output: { event: string } assigns a semantic type to standard event drafts. Omit for free-form { summary } fallback |
sources | Record<string, string | { query: string; context?: boolean }> | no | Named, read-only SQL sources. Results arrive separately in the knowledge payload. Use context("…") (or the raw { query, context: true }) to supply reasoning context without adding rows to the window’s event set |
notification | { channel?, priority? } | no | channel: canvas | notification | both; priority: low | normal | high |
minCooldownSeconds | number | no | Minimum seconds between firings |
tags | string[] | no | Free-form tags |
reactionsGuidance | string | no | LLM guidance for the automation’s downstream reaction agent |
agentKind | string | no | Agent-kind override for firings (e.g. background, notifier) |
reaction | ReactionSource | no | A 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 automation fires. The script must export default async (ctx, client) => …. See the Reaction SDK |
import type weeklyDigestReaction from "./reactions/weekly-digest.reaction.ts";
const digest = defineAutomation({ agent: crm, slug: "weekly-digest", name: "Weekly digest", triggers: [every("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.",});Trigger and source shorthands
Section titled “Trigger and source shorthands”Three factories emit the exact canonical objects shown above — lobu apply sees
the same JSON whether you use them or write the raw literal:
| Factory | Emits | Use for |
|---|---|---|
every(cron, opts?) | a schedule trigger | triggers: [every("0 9 * * 1", { timezone: "Europe/Istanbul" })] |
on(connectorKey, eventType, opts?) | a connector event trigger | triggers: [on("slack", "message.created", { match: { channel_id: "#support" } })] |
context(query) | a context-only SQL source | sources: { candidates: context("SELECT id, … FROM entities …") } |
Connector keys and event types are separate on arguments because connector
keys may contain dots (google.gmail); pass an array of event types to listen
to several. The raw literals remain valid anywhere the shorthand is used. There
is no shorthand for a workspace-source trigger — write
{ kind: "event", source: "workspace", event_types: [...] } directly.
defineConnection(connection)
Section titled “defineConnection(connection)”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.
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Stable slug, the diff key |
connector | string | ConnectorClass | yes | Connector key, or the class produced by defineConnector |
name | string | no | Display name |
authProfile | AuthProfile | string | no | Runtime/account auth profile (handle or slug) |
appAuthProfile | AuthProfile | string | no | OAuth-app auth profile (handle or slug) |
config | Record<string, unknown> | no | Connector configuration (e.g. { botToken: secret("SLACK_BOT_TOKEN") } for a self-hosted chat bot) |
credentialMode | "byo" | "hosted" | "managed" | no | Where 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) |
surfaces | Array<"dm" | "channel"> | no | Hosted chat only: which surfaces a /lobu link code may bind. Default ["dm"] |
codeTtlMinutes | number | no | Hosted chat only: claim-code TTL in minutes. Default 15 |
managedBy | { org } | no | Mark the connection as managed by a cloud org — the OAuth grant lives there; the instance fetches a fresh token at runtime. Set via managedBy, not usually by hand |
deviceWorkerId | string | no | UUID pinning syncs/actions to a specific device worker |
feeds | ConnectionFeed[] | no | Scheduled feeds. Each is { feed, name?, schedule?, config?, virtual? }, where feed is a feed key from the connector. virtual: true marks a federated feed — rows are read live and never copied into events, so it must not set a schedule |
Declare chat connections here like any other connection. Route a chat to an agent with an Automation channel trigger; for the hosted bot, redeeming the /lobu link code creates that Automation.
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") },});defineAuthProfile(authProfile)
Section titled “defineAuthProfile(authProfile)”Declares an auth profile a connection references. Pass it to defineConfig({ authProfiles: [...] }).
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Stable slug, the diff key |
connector | string | ConnectorClass | yes | Connector this profile authenticates |
authKind | env | oauth_app | oauth_account | browser_session | yes | Authentication kind |
name | string | no | Display name |
credentials | Record<string, string | SecretRef> | no | Credential 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"), },});secret(name)
Section titled “secret(name)”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
Section titled “Chat platforms”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 Automation.
Lobu memory
Section titled “Lobu memory”Entity types, relationship types, and automations are the memory schema. Declare them with defineEntityType / defineRelationshipType / defineAutomation 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 }). LOBU_MEMORY_URL is available as an optional base-endpoint override for local or custom Lobu deployments.
Validation
Section titled “Validation”npx @lobu/cli@latest validateChecks that lobu.config.ts loads, conforms to the schema, and that skill IDs and provider configuration are valid. Returns exit code 1 on failure.
Related
Section titled “Related”- CLI reference:
lobu apply,lobu run, and friends. - MCP reference: how declared schema becomes tool-accessible memory.
- Automations and Memory: the runtime model behind
defineAutomationanddefineEntityType. - Connector SDK reference: connector and reaction types.