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 -
runa deployment for a givenexternal_user_idand 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.
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) │
└───────────────────┘ └──────────────────────┘ └──────────────────────┘
| Piece | What it is | Analogy |
|---|---|---|
| Deployment | The published, runnable agent. Has a stable id and a publish mode. | The product integration |
| Version | An 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 principal | One of your customers, identified by an external_user_id you choose. | A connected user / entity |
| Integration connection | One real external account (a specific Slack workspace, one Gmail box). | A connected account |
| Connection binding | Maps 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.

Every connection you add shows up in the table with a clear lifecycle:
| Status | What it means | What to do |
|---|---|---|
| Configured | Credentials/OAuth are in place. Ready to use. | Nothing - attach it to an agent. |
| Setup required | The integration is added but not yet authorized. | Open it and finish the connection (key or OAuth). |
| Included | Bundled and available on your workspace by default. | Nothing. |
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.

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:

| Publish mode | Who can run it | Connections |
|---|---|---|
| Private API | Only your workspace | Your own workspace connections |
| Customer app | Your external customers | Each 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 check | Meaning |
|---|---|
| Immutable version is pinned | Customers run a fixed, reviewed version. |
| Frozen runtime is available | The compilation is frozen and replayable. |
| Tool actions are declared | Every toolkit has exact or inferred actions - no open-ended access. |
| Hosted customer setup is supported | Every toolkit has a supported customer connection path. |
| Risk level reviewed | No undeclared high-risk tool actions. |
| Server publish validation | The 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_idis 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 status | What it means | What to do |
|---|---|---|
| Connected | The slot is bound to the customer's account. | Run the deployment. |
| Pending | A connect session was started but not finished. | Re-send them to redirect_url. |
| Missing | No connection for this slot yet. | Start a connect session. |
| Expired | The 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.
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
runanswers "not connected" with an HTTP 400 and the wrapper turns that intoconnection_required; a future release can return theconnect_urlinline 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.
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
runexactly-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.