Behaviors
A behavior is the part of a vertical SaaS tool you used to buy, built once and owned. It is a standing goal — written once in config, applied to every matching event, and run on a schedule you control. Connectors feed events into the log; a behavior reads the new ones, decides what matters, and acts: remember, notify, request approval, or write back to the world.
You are not scripting the steps. You say what to watch for, which agent runs it, what it should remember, and where it should stop and ask a human. Lobu runs the rest of the loop.
The anatomy of a behavior
Section titled “The anatomy of a behavior”A behavior has five moving parts:
- Agent — every behavior belongs to exactly one agent. The agent carries the persona, skills, tools, providers, and guardrails the behavior runs with.
- Trigger — what wakes it. A schedule (cron), a connector event, or both. The trigger says when, never what.
- Prompt — the goal, written once. A Handlebars template rendered with the window’s entities, content, and SQL sources. The prompt is the filter: it tells the agent which signals from a noisy stream actually matter.
- Sources (optional) — named SQL queries against the events log, bound into the prompt. This is how a behavior reads the data a connector wrote.
- Reaction (optional) — a typed TypeScript script that runs after extraction, where the behavior actually acts: post to Slack, write a derived event, call an external API. See Reactions.
Define one
Section titled “Define one”Behaviors live in lobu.config.ts, declared at the project level and owned by an agent via defineBehavior:
import { defineConfig, defineBehavior, reactionFromFile } from "@lobu/cli/config";
const reconciliationMonitor = defineBehavior({ agent: finance, // owning agent (handle or id) slug: "reconciliation-monitor", // stable diff key name: "Reconciliation monitor", triggers: [{ kind: "schedule", cron: "0 6 * * 1-5" }], // weekdays, 6am notification: { priority: "high", channel: "both" }, tags: ["finance", "reconciliation", "daily"], minCooldownSeconds: 3600, // never fire more than once an hour reaction: reactionFromFile("./reconciliation-monitor.reaction.ts"), prompt: "Check accounts for unreconciled transactions, new variances, and approaching reporting deadlines. Lead with exceptions that need review.",});lobu apply diffs this against the org’s current state and reconciles — a behavior is versioned config, not a script you deploy by hand.
Reading events with sources
Section titled “Reading events with sources”A behavior with no sources only sees the event window that triggered it. To reason over history — the last 200 Gmail messages, this week’s Stripe charges, a customer’s full timeline — add named SQL sources. Each source is a query against the append-only events log, and its result is bound into the prompt by name:
const gmailTxBehavior = defineBehavior({ agent: personal_finance, slug: "gmail-tx", name: "Gmail financial-event extractor", triggers: [{ kind: "schedule", cron: "*/30 * * * *" }], sources: { gmail_messages: "SELECT id, title, payload_text, occurred_at FROM events WHERE connector_key = 'google.gmail' ORDER BY occurred_at DESC LIMIT 200", }, prompt: `Scan the user's forwarded Gmail for events that matter to a UK Self Assessment return.
## Recent emails{{#if sources.gmail_messages}}{{sources.gmail_messages}}{{else}}No new messages this window.{{/if}}
Be conservative: skip noise (marketing, password resets). Extract transactions, disposals, dividends, and documents.`,});Sources are read-only views over the log. The behavior never mutates them; it reads, decides, and writes new events or entities.
Remembering: entity-typed behaviors
Section titled “Remembering: entity-typed behaviors”A behavior can be entity-typed. Set keyingConfig.entityType and the behavior’s output schema derives from that entity type, with extracted rows keyed and merged into entities of that type across windows. This is how a finance behavior turns a stream of raw transactions into a single, corrected Company:Acme record over time — the same record every other agent and behavior reads.
Without keyingConfig, a behavior runs the worker’s free-form { summary } fallback: it observes and summarizes without promoting durable entities. Reach for entity typing whenever two behaviors (or a behavior and a human) need to converge on the same fact. See Memory for why shared, queryable entity memory is the moat.
The loop closes on itself
Section titled “The loop closes on itself”Every action a behavior takes emits a new event back into the log — a note.created, a ticket.opened, a message.sent. Those events are the audit trail, and they are also what the next behavior might pick up on its next pass. A revenue behavior that drafts a recovery note leaves an event a support behavior can read an hour later. That is the whole point of drawing the loop as a circle: facts compound on shared memory instead of getting siloed in ten tools that never talk.
Events are append-only. Corrections supersede the old row rather than deleting it, so the record stays honest and the current view stays clean.
When to reach for a behavior
Section titled “When to reach for a behavior”- Continuously — you want something watching a stream (signups, payments, errors, a queue) on a cadence, not waiting to be asked.
- On a goal, not steps — the decision is a judgment call (is this account at risk? does this need a human?), not a fixed script.
- With shared memory — the output should land on an entity other agents and behaviors read.
- With guardrails — some actions should require approval before they touch the world.
If you only need a working directory for one session, the filesystem is enough. If you need a one-shot answer to a question, chat the agent. A behavior is for the standing work that used to be a separate tool.
- Memory — why entity memory makes behaviors compound
- Reactions — typed code that runs after a behavior extracts
- Connectors — the feeds that fill the events log a behavior reads
- Architecture and concepts — runs, events, entities, and behaviors in the canonical model