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 startsHow you supply the definition
Your product initiates it: an inbound chat message, a webhook you handleAn inline config block on the invoke request (step 1)
Mobius initiates it: a schedule or a trigger on a loopA 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.invoke permission. 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.
  • Admin or Owner membership 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 toolkits and skills: 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[].actions must 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.
  • config sticks 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 one config at a time; to run two definitions at once, give each its own session_key or send session.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 config value is clamped to what policy allows. A definition can narrow behavior but never widen it past org policy. The serialized config is 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"
}
FieldMeaning
sourceclient_resolver to resolve from your endpoint, or mobius_stored (the default) to use Mobius-stored agents. Required.
endpoint_urlThe https URL Mobius POSTs at run start. Required when source is client_resolver; plain http and hostless URLs are rejected.
auth.tokenThe 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_msPer-request resolve timeout. Defaults to 2000; the maximum is 30000.
revalidate_after_sServe 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_sHow long a last-known-good definition stays servable while your endpoint is down. 0 (the default) is unbounded.
on_unavailablelast_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 digest plus a value. 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's known_digest still 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 agent object (agent.instructions, agent.model, agent.effort, agent.timeout_seconds), with toolkits and skills as siblings. Inline config puts those same fields at the top level. Copying the flat config shape 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 200 with the JSON body. Any non-200 status, 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.id is always present, so it works as a mapping key from day one.
  • project.external_ref is 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 the stale_max_age_s bound you set.
  • fail refuses 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