For developers
A workspace,
defined in code.
Connect tools. Run Automations. Build shared memory.
Take approved actions and inspect the evidence.
From a support ticket
to a shared understanding.
Sample Meridian Labs data and IDs; implement the Supportdesk provider adapter and OAuth registration for your source. This preview makes no workspace calls or provider writes.
01 / 6In this project
01Connect
Connect your tools.
Start with a connector and an authenticated connection. OAuth, API keys, browser sessions and device access cover different sources; each connector implements the paths it supports. Here, Supportdesk uses OAuth. Authentication and the choice to collect data are separate.
Complete the OAuth grant in Lobu; credentials stay out of project files.
Details
API-key connectors declare env_keys fields and use an env auth profile with secret references. Browser connectors declare exact HTTPS origins for delegated access; that resource does not choose worker placement. Device-bound connectors declare their device requirement. These are implemented capabilities, not interchangeable auth labels.
Auth & access contractslobu.config.tsconst supportConnector = connectorFromFile(
"./connectors/supportdesk.connector.ts"
);
const supportAccount = defineAuthProfile({
slug: "support-account", connector: supportConnector,
authKind: "oauth_account",
});
const support = defineConnection({
slug: "support", connector: supportConnector,
authProfile: supportAccount,
feeds: [{
feed: "tickets", name: "Enterprise tickets",
config: { queue: "enterprise" },
schedule: "*/15 * * * *", // Optional collection cadence
}],
});
02Read or collect
Read live or collect.
Read native ticket rows on demand, or optionally sync selected changes into shared history. This hybrid example does both. Collection awaits the events and checkpoint together, then reports complete or more. The provider-verified account domain gives each ticket a reliable subject.
The feed’s 15-minute collection cadence is independent of the Automation schedule.
Details
For live-only access, use read without invoking sync. Set schedule: null to clear cron; omitting schedule leaves an existing cadence unchanged. Clearing cron does not disable webhooks or other triggers. This connector emits ticket.updated during sync only. Listening without collecting requires a connector implementing observe. Live reads do not implicitly copy rows to memory; operational audit is separate.
Read, sync & observeconnectors/supportdesk.connector.ts sync: async (ctx) => {
const page = await supportApi.changes({
queue: ctx.config.queue, after: ctx.checkpoint?.after,
credentials: ctx.credentials,
});
await ctx.commit(page.tickets.map(ticket => ({
origin_id: `tickets:${ctx.feedId}:${ticket.id}`,
origin_type: "ticket", title: ticket.title,
payload_text: ticket.body,
metadata: { account_domain: ticket.verifiedAccountDomain },
occurred_at: new Date(ticket.updatedAt),
automation_signals: [{
event_type: "ticket.updated", label: ticket.title,
input_text: ticket.body, resource_ref: ticket.id,
resource_type: "ticket",
}],
})), page.checkpoint);
return { status: page.more ? "more" : "complete" };
},
03Run
Run on events or a schedule.
A collected ticket update or weekday schedule starts the same Automation. Its agent processes a bounded execution window with stored account context. Declared outputs update company risk, save a current summary and append meaningful observations. Trigger it manually through the API too.
Details
The event trigger references the support connection and uses window execution for durable outputs. A declared workspace event output can activate another Automation; arbitrary SQL reads and ordinary knowledge saves are not subscriptions. A reaction is optional for an additional effect, such as a notification, and is unnecessary for persistence or views.
Automation triggers & outputsautomations/account-health.tsexport const accountHealth = (agent: Agent, support: Connection) =>
defineAutomation({
slug: "account-health", agent, skills: ["account-health"],
prompt: "Assess account risk; preserve evidence and summarize changes.",
triggers: [
every("0 9 * * 1-5", { timezone: "Europe/London" }),
on("demo.support", "ticket.updated", {
connection: support, execution: "window",
}),
],
sources: {
tickets: "SELECT id, payload_text, metadata FROM events WHERE origin_type = 'ticket' AND connector_key = 'demo.support'",
accounts: context("SELECT id, name, metadata FROM entities WHERE entity_type = 'company'"),
},
outputs: {
accounts: { entity: company, key: ["domain"] },
summaries: { event: "summary", key: ["account_domain"] },
observations: { event: "observation" },
},
});
04Remember
Build shared memory.
Keep current account state, typed owner relationships and evidence together. The skill resolves verified domains to company IDs and attaches summaries to those records. A keyed summary replaces the current assessment while retaining superseded content, so people and agents can inspect how understanding changed.
Internal save_memory / knowledge.save writes memory; provider actions use operations.execute.
Details
Entity records and graph relationships can change. Meaningful event history retains earlier versions. knowledge.read can include_superseded for an audit or read exact returned IDs while semantic indexing is pending; knowledge.search finds relevant context. Corrections use a new idempotency key and supersedes_event_id. Avoid turning volatile counters into permanent narrative history.
Entities & event kindsschema.tsexport const company = defineEntityType({
key: "company", name: "Company",
required: ["domain", "risk"],
properties: {
domain: { type: "string" },
risk: { type: "string", enum: ["healthy", "at_risk"] },
},
eventKinds: {
summary: { description: "Current account assessment with evidence." },
observation: { description: "A material change in account risk." },
},
});
export const person = defineEntityType({ key: "person", name: "Person" });
export const ownerOf = defineRelationshipType({
key: "account-owner-of", name: "Account owner of",
rules: [{ source: person, target: company }],
});
05Act
Take actions with approval.
When evidence warrants an escalation, the agent discovers the allowed provider operation and requests it with a stable idempotency key. Policy determines whether review is required. Track the operation receipt through execution; an approval alone does not mean the support note was sent.
Requested → pending approval when required → approved → completed or failed. Rejected requests stop.
Details
Use get_approval’s review UI or URL for a human decision, then inspect operations.getRun. Rejection stops the request. Approved work can still fail at the provider. The API example requests a note; it never treats a pending receipt as success.
Operation lifecycleagents/account-health/SKILL.md---
name: account-health
description: Maintain evidence-backed account health.
---
# Account health
Read the triggering tickets and company context; use feeds.readMany for fresh detail.
Resolve account_domain from the provider-verified domain; never match names alone.
Return accounts, summaries and observations arrays; omit unchanged observations.
Each accounts row contains domain and risk (healthy or at_risk).
Each summary/observation has title, explicit entity_ids and content citing evidence IDs.
Set summary metadata.account_domain to the verified domain for stable identity.
Preserve useful evidence and unresolved questions; avoid copying whole threads.
Respect entity write policy and human-owned fields. Inspect pending approvals.
# Provider actions
Discover add_note with operations.listAvailable; check its target and input schema.
Request only an allowed escalation via operations.execute when follow-up is needed.
Use a stable idempotency key: account domain + ticket ID + action + revision.
When policy requires review, report pending_approval and obtain get_approval review UI.
Wait for approval or rejection; inspect operations.getRun for completed or failed.
Pending or approved is never proof that the provider action completed.
06Inspect
Inspect in a view.
Attach a TSX view to the company record and read its current summary through the host. The same memory powers the agent and the interface. Display the assessment and evidence, handle loading or errors, and refresh when needed. There is no separate business-state store to maintain.
Meridian Labs · at risk
SSO is blocking rollout before renewal. Evidence: SUP-104 reports failed SSO; SUP-108 confirms the rollout deadline. Next: request an escalation note.
Details
defineView declares placement; viewFromFile registers the module in lobu.config.ts. useScope supplies the selected company and useQuery performs an authorized read. This view is read-only; approval decisions belong to the real review flow.
@lobu/viewsviews/account-health.tsximport { defineView, useScope, useQuery, tool } from "@lobu/views";
type Assessment = { title: string; payload_text: string };
export const view = defineView({
key: "account-health",
attach: [{ type: "company", surface: "record", placement: "tab" }],
});
export default function AccountHealth() {
const { entity } = useScope();
const read = useQuery<{ content: Assessment[] }>(
typeof entity === "number" ? tool("read_knowledge", {
entity_id: entity, semantic_type: "summary", sort_by: "date", limit: 1,
}) : null
);
const assessment = read.data?.content[0];
const body = read.loading ? "Loading…" : assessment?.payload_text ?? "No summary yet.";
return <article>
<h2>{assessment?.title ?? "Account health"}</h2>
{read.error ? <p role="alert">{read.error}</p> : <p>{body}</p>}
{read.truncated && <p>Partial result. Open the source record.</p>}
<button onClick={read.refetch} disabled={read.loading}>Refresh</button>
</article>;
}
Apply
Review. Then run.
From your project directory, review the diff and prerequisites, then apply the definitions and confirm the CLI plan.
lobu apply --dry-run
lobu applyUse your agent or code.
Read through MCP, call the HTTP client from your server, or query SQL. Runtime examples use your authorized workspace.
query_sdk · Lobu supplies the sandbox client.
export default async (_ctx, client) => {
const workspace = await client.org("meridian-demo");
const live = await workspace.feeds.readMany({
reads: [{ feed_id: 71, query: "status:open", limit: 20 }],
});
const memory = await workspace.knowledge.search({
query: "renewal risk", entity_id: 42, include_content: true,
});
return { live, memory }; // Check each feed's ok / next_cursor.
};