Skip to main content

Embed an Agent in Your Product

You built an agent. Now you want it inside your product - a "Summarize my inbox" button, an in-app assistant, a background worker your users never see. The hard part isn't the agent. It's that every one of your customers brings their own accounts: their Slack workspace, their Notion, their CRM. Customer A's runs must touch Customer A's data and nobody else's.

That is exactly what an embedded deployment gives you:

  • One immutable version you freeze, review, and publish - your customers all run the same reviewed logic.
  • Per-customer connections - each customer authorizes their own accounts once; their runs are pinned to their connections and isolated from every other customer.
  • One HTTP call from your backend - run a deployment for a given external_user_id and get a result. No agent loop, no tool layer, no per-tenant plumbing to build.

If you have used Composio's connected accounts or Stripe Connect, the mental model will feel familiar: you own the app-level configuration; each of your customers owns their own connection state, keyed by an id you choose.

When you need this

Use an embedded deployment when the agent runs on behalf of your end users and each user has their own accounts. If the agent only ever uses your workspace credentials, publish it as a Private API instead (same flow, simpler) - see Serverless Agents.


How it fits together

Six pieces. Read this once and the rest of the guide is just filling them in.

                      ┌─────────────────────── your product ───────────────────────┐
│ │
┌────────────┐ │ POST /deployments/{id}/run │
│ Deployment │◀─────┼── { external_user_id: "customer_42", variables: {...} } │
│ (published)│ │ │
└─────┬──────┘ └─────────────────────────────────────────────────────────────┘
│ pins

┌────────────┐ declares ┌─────────────────┐
│ Version │─────slots────▶│ Access slot │ "this agent needs a
│ (immutable)│ │ (subgroup) │ 'mailbox' and a 'crm'"
└────────────┘ └────────┬─────────┘
│ each customer binds

┌───────────────────┐ binds ┌──────────────────────┐ points to ┌──────────────────────┐
│ External principal │──slot────▶│ Connection binding │──────────────▶│ Integration connection│
│ external_user_id │ │ (principal + slot) │ │ (one real account) │
└───────────────────┘ └──────────────────────┘ └──────────────────────┘
PieceWhat it isAnalogy
DeploymentThe published, runnable agent. Has a stable id and a publish mode.The product integration
VersionAn immutable, frozen snapshot of the agent (instruction + declared tools). Candidate vs active.A pinned release
Access slot (subgroup)A logical slot a version declares - "I need a mailbox". Named by you.An auth config
External principalOne of your customers, identified by an external_user_id you choose.A connected user / entity
Integration connectionOne real external account (a specific Slack workspace, one Gmail box).A connected account
Connection bindingMaps one principal + one slot → one connection. This is what isolates customers.user → account link

The key idea: a version declares slots, and each customer fills each slot with their own connection. Customer A's mailbox slot points at Customer A's Gmail; Customer B's points at Customer B's. Same frozen logic, different data, fully isolated.


Step 1 - Add the MCPs your agent needs

Open MCPs/Tools in the left nav and hit + Connect MCP integration. Search the catalog (600+ integrations, each reviewed by the FlyMyAI team) or add your own with Add custom MCP.

Connecting an MCP from the catalog, with the integration table behind it

Every connection you add shows up in the table with a clear lifecycle:

StatusWhat it meansWhat to do
ConfiguredCredentials/OAuth are in place. Ready to use.Nothing - attach it to an agent.
Setup requiredThe integration is added but not yet authorized.Open it and finish the connection (key or OAuth).
IncludedBundled and available on your workspace by default.Nothing.
Your credentials never reach the model

Connection credentials are AES-256 encrypted at rest, are never exposed to the LLM, and are only decrypted inside an isolated sandbox at execution time. The model sees tool results, not tokens.

Then attach the tools to your agent. In the agent chat, the Creation tools panel lists every integration with its status (Ready / Needs setup) - pick the ones this agent should be allowed to call. Attaching a tool is what makes the frozen version declare a slot for it in the next step.


Step 2 - Freeze an immutable version

Embedded runs are always pinned to a frozen version - your customers can never be served a half-edited draft.

In the agent's right rail, open Access and publish and hit Prepare access. The platform freezes the current chat into an immutable version and stages it as a candidate. Publishing is a two-step flow: 1. Access → 2. Publish.

The Access step: review each toolkit and account rule before publishing

Candidate vs active. A candidate version is staged without changing live traffic - your already-published customers keep running the current active version until you press Publish. This is how you roll out a change safely.

Runtime access

The Access step lists every toolkit slot the version needs and how it will be satisfied. Two shapes:

  • Connectionless - the toolkit runs on shared/workspace credentials and needs no per-customer authorization (e.g. web search). You will see "This agent does not require external toolkit access."
  • Hosted customer setup - the toolkit needs each customer to bring their own account. Each such toolkit becomes an access slot your customers fill in Step 5.

Declaring access is deliberate: you review exactly which accounts the frozen agent may touch before a single customer can run it.


Step 3 - Publish

Switch to 2. Publish and choose a publish mode:

The Publish step: choose Customer app, review the preflight checklist, copy the SDK snippet

Publish modeWho can run itConnections
Private APIOnly your workspaceYour own workspace connections
Customer appYour external customersEach customer gets isolated connections

Pick Customer app for an embedded, multi-tenant integration. Before it lets you publish, a preflight checklist has to be green:

Preflight checkMeaning
Immutable version is pinnedCustomers run a fixed, reviewed version.
Frozen runtime is availableThe compilation is frozen and replayable.
Tool actions are declaredEvery toolkit has exact or inferred actions - no open-ended access.
Hosted customer setup is supportedEvery toolkit has a supported customer connection path.
Risk level reviewedNo undeclared high-risk tool actions.
Server publish validationThe server accepted this immutable release.

Press Publish version. You now have a live deployment with a stable Deployment ID - copy it, that's what your backend calls.


Step 4 - Run it for a customer

From your backend, run the deployment for one of your users. The external_user_id is any stable id from your side - a user id, a tenant id, an account id. It is the whole multi-tenant key: FlyMyAI creates an external principal for it on first use and pins that run to that customer's connections.

# pip install flymyai
from flymyai import AgentClient

client = AgentClient(api_key="fly-***") # your server-side key - never ship it to the browser

run = client.deployments.run(
"103cc935-3074-4c99-8628-b557259f49cd", # Deployment ID from Step 3
external_user_id="customer_42", # YOUR id for this customer
variables={"since": "2026-08-01"}, # inputs your frozen version declared
idempotency_key="req_8f21", # optional - safe retries
)

print(run.status) # completed | failed | running
print(run.output)

Over raw HTTP it is the same call:

curl -X POST https://backend.flymy.ai/api/v1/agents/deployments/103cc935-.../run/ \
-H "X-API-KEY: fly-***" \
-H "Idempotency-Key: req_8f21" \
-H "Content-Type: application/json" \
-d '{"external_user_id": "customer_42", "variables": {"since": "2026-08-01"}}'
  • external_user_id is required and isolates the run. Two customers with two ids can never see each other's connections or data.
  • Idempotency-Key (header, ≤255 chars) makes retries safe: the same key on the same deployment returns the original run instead of starting a new one.
  • The active frozen version is selected server-side. Keep API credentials on your server - the browser never needs the key.

Step 5 - Let each customer connect their accounts

If your agent uses a toolkit that needs per-customer authorization, the customer authorizes it once, and every later run reuses that connection. The flow is the familiar async initiate → redirect → authorize pattern.

1. Start a connection session for the customer and the slot they need to fill:

session = client.deployments.connect_session(
"103cc935-3074-4c99-8628-b557259f49cd",
external_user_id="customer_42",
slot="mailbox", # the access slot from Step 2
alias="Work Gmail", # a label your customer will recognize
)
# -> { "redirect_url": "https://...", "expires_at": "...", "provider": "composio" }

2. Send the customer to redirect_url. They log in to their own Slack / Gmail / CRM and approve. Thread your own context through your return URL so you know who came back - exactly like an OAuth callback:

https://your-app.com/connected?customer_id=customer_42&slot=mailbox

3. Check the binding before you run:

access = client.deployments.access(
"103cc935-3074-4c99-8628-b557259f49cd",
external_user_id="customer_42",
)
Binding statusWhat it meansWhat to do
ConnectedThe slot is bound to the customer's account.Run the deployment.
PendingA connect session was started but not finished.Re-send them to redirect_url.
MissingNo connection for this slot yet.Start a connect session.
ExpiredThe account's authorization lapsed.Start a fresh connect session to re-auth.

Connectionless toolkits skip all of this - there is nothing for the customer to authorize.


The integration loop: one call, connect on demand

You do not decide up front whether to call connect or run. Always call run. If the customer has not connected a required account yet, the run tells you - and you hand that customer a connect link. Wrap it once and reuse it everywhere.

These endpoints are REST today

run, connect-session and access are plain HTTP right now (a client.deployments.* SDK namespace is on the way). The examples above show the intended SDK shape; the loop below is copy-paste and runs today.

import os, time, requests

BASE = "https://backend.flymy.ai/api/v1/agents"
H = {"X-API-KEY": os.environ["FLYMYAI_API_KEY"]} # server-side only
DEPLOYMENT = "103cc935-3074-4c99-8628-b557259f49cd"

def run_or_connect(external_user_id, variables):
"""-> {'status':'running','execution_id':...}
or {'status':'connection_required','connections':[{'slot','connect_url'}]}"""
r = requests.post(f"{BASE}/deployments/{DEPLOYMENT}/run/", headers=H,
json={"external_user_id": external_user_id, "variables": variables})
if r.status_code in (200, 201):
return {"status": "running", "execution_id": r.json()["id"]}
# a required account is not connected -> find which slots, mint a link for each
access = requests.get(f"{BASE}/deployments/{DEPLOYMENT}/access/", headers=H,
params={"external_user_id": external_user_id}).json()
bound = {b["slot"] for b in access.get("bindings", [])}
links = []
for req in access.get("requirements", []):
if req.get("connection_required") and req["slot"] not in bound:
s = requests.post(f"{BASE}/deployments/{DEPLOYMENT}/connect-session/", headers=H,
json={"external_user_id": external_user_id,
"slot": req["slot"], "alias": req["slot"]}).json()
links.append({"slot": req["slot"], "connect_url": s["redirect_url"]})
return {"status": "connection_required", "connections": links}

def wait(execution_id, timeout=120):
for _ in range(timeout // 2):
run = requests.get(f"{BASE}/executions/{execution_id}/", headers=H).json()
if run["status"] in ("completed", "failed"):
return run
time.sleep(2)

In your product it is a two-branch call:

out = run_or_connect("customer_42", {"count": 3})

if out["status"] == "connection_required":
# show the customer a "Connect Gmail" button pointing at this URL
connect_url = out["connections"][0]["connect_url"]
# ... after they authorize in the browser, call run_or_connect again -> it runs
else:
result = wait(out["execution_id"])["agent_result"]

The rule: call run; if it returns connection_required, surface the connect_url to that customer and call run again once they have authorized. The same shape covers first-time connect and re-auth - if a token later lapses, the slot simply comes back connection_required and your existing code re-issues the link. No separate "is my customer connected?" bookkeeping on your side.

Today run answers "not connected" with an HTTP 400 and the wrapper turns that into connection_required; a future release can return the connect_url inline so the branch gets even smaller.


Multiple connections of one type, and subgroups

Two things people reach for that both map onto slots:

One customer, several accounts of the same kind. Say your agent posts to two Slack workspaces - a staff channel and a customers channel. Declare two slots (slack_staff, slack_customers) instead of one. Each customer binds each slot to a different Slack connection. The slot name is how the frozen agent refers to "which one" - the runtime resolves each slot to the exact bound account at run time, so there is never ambiguity about which Slack a message went to.

Many customers, the same slot. A single mailbox slot is filled independently by every external principal. Customer A's mailbox → A's Gmail; Customer B's mailbox → B's Gmail. The ConnectionBinding (principal + slot → connection) is what keeps them isolated - it is unique per (principal, slot), so one customer's binding can never leak into another's run.

Rule of thumb

One slot per role the agent plays, not per customer. You declare slots once on the version; customers scale out horizontally by each filling those same slots with their own accounts.


Reusing an MCP across embedded agents

A connection you add under MCPs/Tools lives on your workspace, not inside a single agent. Attach the same integration to as many agents as you like - each frozen version just declares a slot for it. For per-customer (Customer app) runs, the account is always resolved from the external principal's binding, so reuse never crosses tenants: the same slot definition, filled per customer, on every agent that declares it.

Practical consequences:

  • Rotate a workspace credential once under MCPs/Tools and every agent that uses it picks up the change.
  • Publishing a new candidate version does not disturb customers' existing connections - bindings are keyed to the principal and slot, not to a version.

Security & isolation

  • Per-customer isolation by construction. A run is pinned to one external_user_id; connections resolve only from that principal's bindings.
  • Immutable, reviewed logic. Customers always run a frozen version that passed preflight - no drafts, no undeclared tools.
  • Least privilege. Only the tool actions the version declares are callable. Preflight refuses undeclared high-risk actions.
  • Credentials stay server-side. Keys are AES-256 encrypted, never sent to the model or the browser, and decrypted only inside the execution sandbox.
  • Safe retries. Idempotency keys make run exactly-once from your callers' point of view.

Next steps

  • Serverless Agents - freeze an agent and call it as a Private API (the single-tenant sibling of this guide).
  • Call Your Agent from Your Product - the end-to-end integration path, from first call to production.
  • Tools - the built-in and MCP tools an agent can call, and how it picks them.