Embed
Embed Mobius as your agent backend
When Mobius is the agent backend behind your product, your app decides how each agent behaves and Mobius does the durable work: sessions, turn execution, streaming, and scheduling. The piece your app owns is the definition: the agent's instructions, model, effort, per-turn timeout, toolkits, and skills. Keeping it in your own system means a behavior change ships with your next deploy, with no per-tenant copies in Mobius to update.
This guide wires that up end to end. A run can start from either side, and each side has its own way to hand Mobius the definition:
| How the run starts | How you supply the definition |
|---|---|
| Your product initiates it: an inbound chat message, a webhook you handle | An inline config block on the invoke request (step 1) |
| Mobius initiates it: a schedule or a trigger on a loop | A client resolver endpoint Mobius calls at run start (steps 2 and 3) |
Both paths produce the same definition, and Mobius runs it the same way no matter which path supplied it. The agent in Mobius stays the durable anchor for identity, memory, sessions, and run history. Only its behavior lives in your system.
What you'll build
- An invoke integration that starts an agent turn for each inbound message and sends your current definition with the request.
- A client resolver endpoint that returns your current definition when Mobius starts a scheduled run.
- A tenant mapping from each Mobius project to one of your workspaces, and a degradation policy so scheduled runs survive a brief resolver outage.
Prerequisites
- An API client with an
API key that holds the
mobius.agent.invokepermission. The invoke path in step 1 authenticates with it. - An agent in the project to anchor sessions and history. Its stored definition is the fallback; anything you send overrides it per turn.
AdminorOwnermembership in the org. The resolver changes where every run in the org gets its definition, so registering it takes an admin.- A place in your own backend that already stores each agent's definition: the source of truth you want Mobius to read from.
Set MOBIUS_BASE_URL and MOBIUS_API_KEY in your environment for the
examples below.
Step 1: Invoke with an inline definition
For runs your product initiates, add a config block to the invoke call.
Mobius resolves or creates the session,
appends your input, and runs one turn on the definition you sent.
const response = await fetch(
`${process.env.MOBIUS_BASE_URL}/v1/projects/platform/agents/invoke`,
{
method: "POST",
headers: {
authorization: `Bearer ${process.env.MOBIUS_API_KEY}`,
"content-type": "application/json",
accept: "text/event-stream",
},
body: JSON.stringify({
agent_ref: { id: "agt_scout" },
session: {
mode: "continue_or_create",
session_key: "app:acct_123:user_456:support",
title: "Support chat",
},
config: {
instructions: "You are Acme's support agent. Be concise and cite ticket numbers.",
model: "claude-sonnet-4-6",
effort: "medium",
timeout_seconds: 120,
toolkits: [
{ name: "tickets", actions: ["tickets.search", "tickets.get"] },
],
skills: [
{ name: "refunds", description: "How to issue a refund", body: "..." },
],
},
input: {
content: [{ type: "text", text: "Summarize my open tickets." }],
idempotency_key: "slack:evt_0a1b2c3d",
},
}),
},
);Four rules govern config:
- Every field is optional and overrides on its own. A field you set
replaces the agent's stored value; a field you omit inherits it. The
exceptions are
toolkitsandskills: a list you send replaces the agent's list whole, because a partial merge would make the final tool surface depend on state you can't see in the request. - Toolkits select tools, they don't create them. Each name in
toolkits[].actionsmust be an action already in the project's catalog (built-ins and connected integrations). Names outside the catalog are ignored, so a definition can narrow the agent's tools but never add new ones. configsticks to the session. Mobius stores it on the resolved session, so you send it once and later turns reuse it. Send it again to replace it, omit it to keep the current one, or send an empty{}to revert to the agent's stored definition. A session holds oneconfigat a time; to run two definitions at once, give each its ownsession_keyor sendsession.mode: "new". A stateless client can simply send the full document on every call.- Org limits still apply. If your organization caps the model, effort, or
timeout, a
configvalue is clamped to what policy allows. A definition can narrow behavior but never widen it past org policy. The serializedconfigis capped at 256 KB, which is plenty for real instruction sets while keeping requests cheap to validate.
The invoke endpoint has two response modes: an inline stream of server-sent
events (SSE), shown above, or a 202 acknowledgement you then stream from.
Invoke agents from your backend covers
streaming, idempotency, and reconnect mechanics in full.
That's the whole invoke path. Every conversation your product starts now runs on the definition your app sent, and a behavior change is just different bytes on the next call.
Step 2: Register a client resolver
A schedule or trigger has no request to carry config, so point your org at
an HTTPS endpoint Mobius calls at run start instead. Register it once with a
full-replace PUT against your active organization:
PUT /v1/organization/definition-resolver{
"source": "client_resolver",
"endpoint_url": "https://api.acme.example/mobius/resolve",
"auth": { "mode": "bearer", "token": "shared secret Mobius sends on each call" },
"timeout_ms": 2000,
"revalidate_after_s": 30,
"stale_max_age_s": 3600,
"on_unavailable": "last_known_good"
}| Field | Meaning |
|---|---|
source | client_resolver to resolve from your endpoint, or mobius_stored (the default) to use Mobius-stored agents. Required. |
endpoint_url | The https URL Mobius POSTs at run start. Required when source is client_resolver; plain http and hostless URLs are rejected. |
auth.token | The shared bearer token Mobius sends on every resolve so your endpoint can verify the caller. Write-only: send it to set or rotate it, omit auth to keep the current one, or send an empty string to clear it. |
timeout_ms | Per-request resolve timeout. Defaults to 2000; the maximum is 30000. |
revalidate_after_s | Serve the cached definition with no network call while it's younger than this many seconds. Omit or 0 to revalidate on every run. |
stale_max_age_s | How long a last-known-good definition stays servable while your endpoint is down. 0 (the default) is unbounded. |
on_unavailable | last_known_good (the default) or fail. See keep runs alive when your resolver is down. |
The PUT is a full replace: any field you omit reverts to its default. The
one exception is auth. Omit it and Mobius keeps the token you already sent,
so routine edits never resend the secret.
The response (also what a GET to the same path returns) confirms the
registration, with the token redacted:
{
"source": "client_resolver",
"endpoint_url": "https://api.acme.example/mobius/resolve",
"auth_configured": true,
"timeout_ms": 2000,
"protocol_version": 1,
"revalidate_after_s": 30,
"stale_max_age_s": 3600,
"on_unavailable": "last_known_good",
"last_good_at": null,
"updated_at": "2026-07-05T12:00:00Z"
}A null last_good_at right after registration is normal: last_good_digest
and last_good_at fill in once the first resolve succeeds, and from then on
they show what Mobius last cached from your endpoint. Both calls require
Admin or Owner membership. To stop resolving from your endpoint later,
send source: "mobius_stored".
Step 3: Implement the resolve contract
At run start Mobius POSTs your endpoint with
Authorization: Bearer <token> and a JSON body:
{
"protocol_version": 1,
"org_id": "org_123",
"project": { "id": "prj_456", "external_ref": "workspace_789" },
"trigger": { "kind": "schedule" },
"refs": [{ "selector": "agent/scout", "known_digest": "sha256:abc..." }]
}trigger.kind tells you why the run started (for example schedule). Treat
it as context you may branch on, not an exhaustive enum. refs carries one
agent per call today; iterate it anyway so your handler keeps working when
batching arrives.
Your endpoint verifies the bearer token, maps the tenant, and returns a
resolved map keyed by each ref's selector:
// POST /mobius/resolve on your backend
app.post("/mobius/resolve", (req, res) => {
if (req.get("authorization") !== `Bearer ${process.env.MOBIUS_RESOLVER_TOKEN}`) {
return res.sendStatus(401);
}
const { project, refs } = req.body;
const tenant = lookupTenant(project.external_ref || project.id);
const resolved = {};
for (const ref of refs) {
const def = tenant?.definitionFor(ref.selector); // your source of truth
if (!def) {
// Fail the run with a reason rather than run an empty definition.
resolved[ref.selector] = { error: { code: "unknown_agent" } };
} else if (def.digest === ref.known_digest) {
// Mobius' cache is current; skip re-sending the body.
resolved[ref.selector] = { unchanged: true };
} else {
resolved[ref.selector] = {
digest: def.digest,
value: {
agent: {
instructions: def.instructions,
model: def.model,
effort: def.effort,
timeout_seconds: def.timeoutSeconds,
},
toolkits: def.toolkits, // [{ name, actions: ["crm.lookup"] }]
skills: def.skills, // [{ name, description, body }]
},
};
}
}
res.json({ resolved });
});Each ref resolves to exactly one of three outcomes:
- A fresh definition: a
digestplus avalue. The digest is yours to choose. Mobius stores no pins, so varying the digest per cohort in your resolver is a canary, and a rollout is a branch in your code, not a migration in Mobius. { "unchanged": true }: the ref'sknown_digeststill matches your current definition, so Mobius serves its cached copy and you skip the body.{ "error": { "code": "..." } }: the run fails to start with your reason string rather than running an empty definition.
Note: A resolver response nests the behavior fields under an
agentobject (agent.instructions,agent.model,agent.effort,agent.timeout_seconds), withtoolkitsandskillsas siblings. Inlineconfigputs those same fields at the top level. Copying the flatconfigshape into a resolver response is the most common mistake; it leaves the agent's behavior empty.
Mobius enforces a few transport rules on every resolve:
- Respond
200with the JSON body. Any non-200status, a malformed body, or a response missing a requested ref counts as unavailable, and your degradation policy applies (see keep runs alive when your resolver is down). - The response body is capped at 4 MiB, far more than a definition needs.
- The endpoint must be
https. Mobius refuses URLs that point at loopback, link-local, private, or otherwise reserved addresses (including cloud metadata endpoints), pins the connection to the validated address so a DNS rebind can't redirect it, and never follows redirects. The bearer token is encrypted at rest and never returned by the API.
To confirm the whole path works, let a scheduled run fire (or start one
manually), then GET the resolver config from step 2 again: last_good_digest
now shows the digest your endpoint returned.
Map tenants to your workspaces
Every resolve carries the Mobius project.id and your project.external_ref.
Map on whichever you have wired:
project.idis always present, so it works as a mapping key from day one.project.external_refis a durable correlation key you set to your own tenant or workspace id when you provision the project. It's immutable and unique per org, and it's the same key you use to look up or audit the Mobius project from your own system later. We recommend setting it at provisioning time so neither system needs a lookup table for the other.
The bearer token proves the caller is Mobius, nothing more. Never derive tenant identity from it.
Keep runs alive when your resolver is down
If your endpoint is briefly unreachable, a scheduled run doesn't have to
fail. The on_unavailable policy decides what happens:
last_known_good(the default) serves the last successful definition, which Mobius keeps durably in Postgres, for up to thestale_max_age_sbound you set.failrefuses to start the run and records a clear reason. Choose it when running on stale instructions is worse than not running at all.
Either way, Mobius never silently runs an empty definition. A definition
younger than revalidate_after_s also skips the network entirely, so a short
revalidation window keeps run starts fast without a fetch on every fire.
When in doubt, keep last_known_good and set stale_max_age_s to an hour,
as in the example above.
How the two paths layer
An invoke turn and a scheduled run resolve from the same org source. Before
any turn runs, Mobius resolves the base definition from your org's configured
source (the stored agent by default, or your client resolver once one is
registered), then merges any inline config over it. So after you register a
resolver, invoke turns resolve through it too, and config overrides just the
fields you send. Precedence on every turn is inline config, then the
resolved base, all clamped by org limits.
Not yet supported
Definitions can only select existing catalog actions through toolkits and
carry skills bodies. Defining brand-new actions or MCP servers in a
definition (the actions and mcp_servers fields) is stored for provenance
but doesn't execute yet, and per-reference ($ref) resolution is a later
addition. To give an agent a new tool, connect the
integration in the project and select its
actions.
Next
- Agent definitions is the canonical reference for the definition shape, sources, precedence, and guarantees.
- Invoke agents from your backend covers the invoke endpoint's streaming, idempotency, and reconnect rules.
- Agent sessions explains transcripts, turns, and the stream frames you render.
- The
getDefinitionResolverandreplaceDefinitionResolverAPI reference pages document the resolver config schema field by field.