# camelAI Documentation --- --- title: 'What is camelCode?' description: 'Describe anything. camelCode builds it, deploys it, keeps it running.' --- camelCode is camelAI's coding agent with a persistent computer. Describe anything, and camelCode builds it, deploys it, and keeps it running. Pick the model you want to use, then chat with an agent that creates full-stack web applications connected to your data, your tools, and AI models. ### [Try camelCode](https://camelai.dev) **No credit card required.** Start building in minutes. ## Platform Capabilities ### Build Full-Stack Apps Chat with the agent to build complete web applications with React, TypeScript, and Tailwind. Production-quality UI is included from the start. ### Publish Instantly Deploy any project to a live URL with one click. DNS, SSL, and CDN handled automatically. ### Persistent Workspace Your files, databases, and projects survive across sessions. Pick up where you left off anytime. ### 50+ Integrations Connect to Postgres, Snowflake, Stripe, Slack, GitHub, Notion, and dozens more. Credentials are securely injected into your apps. ### AI-Powered Apps Build apps that themselves use AI — chatbots, summarizers, content generators. Native AI bindings included in every project. ### Autonomous Agents Schedule cron jobs for recurring tasks. The agent runs autonomously — generating reports, syncing data, monitoring metrics. ## What Can You Build? ### AI-Powered Apps Customer support chatbots, AI writing assistants, lead qualification tools, meeting notes summarizers ### Internal Tools Admin panels, multi-source dashboards, bug report portals — connected to your real data ### Automated Workflows Daily reports from your data warehouse, Slack alerts from Stripe events, hourly metric monitors ### Sites & Forms Landing pages, feedback forms, booking pages, portfolio sites — all with live backends ## Get Started ### Build your first app Follow the [quickstart guide](/docs/getting-started/quickstart) to go from idea to live URL in minutes. ### Explore the platform Learn about your [workspace](/docs/getting-started/workspace), [publishing](/docs/getting-started/publishing), and [connections](/docs/getting-started/connections). ### [Plans & pricing](/docs/plans/overview) What's included on each plan, how credits work, and how to top up. ## Live Examples Every app below was built and deployed entirely by camelAI. ### [WishBoard](https://wishboard--camelai-team-d9e.camelai.app/) A Pinterest-style bookmarking app with automatic Open Graph images ### [Vela — Stripe Dashboard](https://vela-dashboard--tools-558.camelai.app/) 18 months of SaaS revenue, churn, and cohort analysis ### [Loopform Support Bot](https://loopform--camelai-team-d9e.camelai.app/) An AI support bot for a form-building SaaS ### [AI News Daily](https://ai-news-daily--ms-workspace-b3c.camelai.app) A newspaper-style site that posts a roundup of AI news every morning ## camelAI products ### [camelCode](/docs/getting-started/quickstart) Chat with an agent that builds and deploys full-stack apps. You're reading its docs. ### [Embedded Analytics (Legacy)](/docs/dev_api/introduction) Retired iframe analytics product; docs kept for existing customers. --- --- title: 'Build Your First App' description: 'Go from idea to live URL in minutes' --- Let's build a web app and get it live — all from a single conversation. ### Sign up Go to [camelai.dev](https://camelai.dev) and create an account. Free tier available with your own API key. See [plans and pricing](/docs/plans/overview). We automatically set up your organization and workspace — a persistent computer where your agent lives. Everything you build here survives across sessions. ### Chat with your agent After signup, you're dropped right into a conversation. The agent will ask you a few questions to understand what you want to build — just answer naturally. Or jump straight in with a request: > Build me a personal bookmark manager where I can save links with tags, star > my favorites, and search through everything. The agent scaffolds a full-stack app with React, TypeScript, Tailwind, and shadcn/ui — then starts building. You'll see files appear in your workspace as it works. > **Tip:** > > You don't need to be specific about technology choices. The agent picks the right > tools and builds production-quality UI by default. ### It's already live As soon as the agent builds your app, it's deployed to a live URL at `*.camelai.app` — automatically. There's no publish button, no deploy step. DNS, SSL, and CDN are all handled for you. Copy the URL and share it with anyone. Your app is on the internet. > **Info:** > > Apps stay live even when you're not in camelAI. They're real production services > on Cloudflare Workers — zero cold start, globally distributed. ### Keep iterating Your app is live, but you're not done. Keep chatting to add features, fix things, or change the design: > Add a dark mode toggle > Make the search filter by tags too > Change the color scheme to match my brand Changes go live automatically as the agent makes them. ## What's next? ### [Your Workspace](/docs/getting-started/workspace) Understand the persistent computer ### [Your Apps](/docs/getting-started/publishing) URLs, public vs. private, managing apps ### [Connections](/docs/getting-started/connections) Connect to 50+ databases and tools --- --- title: 'Organizations and workspaces' description: 'How camelAI separates billing and access from the actual computer your work lives on.' --- camelAI has two layers of structure: **organizations** and **workspaces**. They serve different purposes, and getting them straight up front saves headaches later. * An **organization** is your billing and access boundary. Your plan, your payment method, your team members, and your LLM provider all attach to the org. * A **workspace** is the actual computer your agent works on. Files, chats, apps, and connections all live inside a workspace. You can have more than one workspace inside an org. If you're a solo user on a single plan, you may never think about this distinction. You sign up, and we provision one org with one workspace, and that's everything. The split matters once you want to keep two projects fully separate, or once you want a teammate to see one workspace but not another. ## What lives where **At the org level** * Plan and payment method * LLM provider and API key (or camelAI hosted credits) * Members, invitations, and roles * Admin controls for any custom domains attached to apps in the org **At the workspace level** * The agent's persistent computer (files, folders, storage) * Chats and chat history * Connections (databases, SaaS tools, APIs) * Published apps and their `*.camelai.app` URLs * The workspace's email address and Slack integration * Cron jobs Anything you create *with* the agent lives in a workspace. Anything that decides *who can do what and who pays for it* lives at the org. ## Organizations An org is a fully separate entity. Two orgs owned by the same person have nothing to do with each other: * Separate plans and billing. * Separate LLM providers. A key you add to Org A does not work for Org B. * Separate members. Adding someone to Org A does not give them any visibility into Org B. * Separate workspaces, files, apps, and connections. You can create a new org any time from [camelai.dev/settings/organizations](https://camelai.dev/settings/organizations). New orgs start on the Free plan and need their own payment setup before you can move to a paid plan. When to create a second org: * You want a hard wall between two contexts. Personal projects on one org, client work on another. * You're a contractor and a client wants to pay for camelAI directly. They create their own org and invite you. * You want completely separate billing for two parts of a company. > **Tip:** > > If you don't need a hard wall, stay on one org and use multiple workspaces instead. That's almost always the right move. ## Workspaces A workspace is a real Linux computer your agent lives on. It is persistent: files survive across sessions, chats stay in their threads, deployed apps keep running. ([More on the workspace itself.](/docs/getting-started/workspace)) Every plan includes at least one workspace: | Plan | Workspaces included | | ---------- | ------------------- | | Free | 1 | | Starter | 1 | | Pro | 1 | | Team | 2 | | Enterprise | Unlimited | Need more than your plan includes? Email **[support@camelai.com](mailto:support@camelai.com)** and we'll add more to your org. Self-serve a la carte workspaces are coming soon. ## Why have more than one workspace Two reasons. ### 1. Project isolation Each workspace is its own computer. Files, connections, and chats from one workspace are not visible from another. Use this when you don't want two projects sharing a filesystem or environment. ### 2. Per-workspace access for teams On the Team plan and above, you can grant individual members access to specific workspaces. The most common use of this is splitting a team by function: * A **Marketing** workspace, with the Stripe, HubSpot, and Mailchimp connections marketing needs. * A **Finance** workspace, with the QuickBooks, BigQuery, and bank connections finance needs. * People on the marketing team get access to the marketing workspace. People on the finance team get access to the finance workspace. Admins keep access to both. Same org, same bill, same team, but the underlying data and connections stay separated by function. If a marketer asks the agent to pull revenue numbers, the agent only has the connections in their workspace to work with. They literally cannot reach finance data, because the credentials aren't in the workspace they're chatting in. ## Roles and team members Multiple seats are only available on the **Team** and **Enterprise** plans. On Free, Starter, and Pro, you're the only person on your org. Org roles: * **Owner.** The person who created the org. Full control, including billing and ownership transfer. * **Admin.** Can invite and remove members, manage workspaces, manage connections, manage app custom domains, and adjust billing. * **Member.** Can chat with the agent and use any workspace they've been granted access to. Can't invite or remove team members. (We're tightening up the exact list of admin-vs-member gating soon, so the precise boundary may shift.) Workspace access on Team plans is granted per workspace, per member. An admin chooses which workspaces each member can see. There's no "viewer vs. editor" distinction inside a workspace: if a member has access, they can do anything the agent can do in that workspace. ## Custom domains **Custom domains are per app.** Each app you publish gets its own `*.camelai.app` URL by default, and you can attach a custom domain to any individual app from its settings. Different apps in the same workspace can have different custom domains, or none at all. Org admins can see and manage custom domains across every app in the org from a single admin view. The domain itself attaches to the app. Plan limits on custom domains (Starter: 10, Pro and up: unlimited) refer to the number of apps that can have one attached. ([Full plan details.](/docs/plans/overview)) ## FAQ ### Does my LLM provider key work across all my orgs? No. LLM provider keys are set per org. If you have two orgs, you'll set up a provider for each one separately. This is intentional: the LLM provider determines who pays for model usage, and orgs have separate billing. ### If I switch workspaces, does my chat history come with me? No. Chats live in the workspace they were created in. Switching to a different workspace gives you that workspace's chat history. ### Can two workspaces share files or connections? No. Workspaces are isolated. If you need the same connection in two workspaces, you can clone any connection to a workspace within your org from the [connections page](https://camelai.dev/connections). ### Can a connection be at the org level instead? No. Connections are workspace-scoped. This is what makes per-workspace access useful: you can give marketing access to a workspace with marketing connections without giving them finance connections. You can clone any connection to a workspace within your org from the [connections page](https://camelai.dev/connections). ### Are custom domains attached to my org or to my apps? To your apps. Each published app can have its own custom domain. Org admins manage them all from one place, but the domain itself belongs to the app. See the section above. ### Can I move an app from one workspace to another? Not currently. If you need an app in a different workspace, the cleanest path right now is to ask the agent to zip the file, download the zip, and then upload that to your workspace. ### Do I need a separate org for personal vs. work projects? Only if you want them billed separately or kept fully isolated. If shared billing is fine, two workspaces inside one org will give you all the project separation you need. ### Who can create a new workspace? Owners and admins. Members can't add workspaces. New workspaces beyond what your plan includes require either an upgrade or a request to [support@camelai.com](mailto:support@camelai.com). ### What does an admin get that a member doesn't? Admins can invite and remove team members, create new workspaces, manage which members have access to which workspaces, manage connections, and manage billing. Members can chat in workspaces they have access to and use any connection the workspace has. ### I created a second org and I don't see my old workspaces. Is that a bug? No. Orgs are fully separate. Workspaces in your first org aren't visible from your second org. Use the org switcher to jump between them. Go to [your org list](https://camelai.dev/settings/organizations) to see which orgs you are a part of. --- --- title: 'Enterprise SSO' description: 'Connect your camelAI organization to Google Workspace, Microsoft Entra ID, Okta, Auth0, or another OpenID Connect provider.' --- Enterprise SSO lets people sign in to a camelAI organization through your identity provider. camelAI supports OpenID Connect (OIDC), including Google Workspace, Microsoft Entra ID, Okta, and Auth0. SSO is available on the Enterprise plan. Organization owners and admins can configure and manage it. ## Before you start You need: * Permission to create an OAuth or OIDC client in your identity provider * Owner or admin access to your camelAI organization * A regular user account in your identity provider for testing ## Choose who can join The **Allow uninvited SSO users** setting controls whether an identity provider login can create a member without a camelAI invitation. | Setting | What happens on the first SSO sign-in | | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Off | Existing organization members and users with an active invitation can sign in. An invited user is created automatically with the role and workspace access selected in the invitation. | | On | Any user allowed by the identity provider and camelAI domain rules can join. camelAI creates them as a regular member with access to the first active workspace. | Leaving this setting off is the more controlled option. Invite each person from your organization Team settings, then send them the SSO sign-in URL. They do not need to create a password-based camelAI account or accept the invitation link first. Their verified SSO login consumes the invitation. ## Configure your identity provider Start in camelAI under **Settings → Organization → Single sign-on**. Copy the callback URL shown on the page and use it exactly in your identity provider. It normally looks like: ```text https://camelai.dev/api/auth/enterprise-oidc/callback ``` Choose your provider below. camelAI uses the issuer, client credentials, token authentication method, and email claim. It automatically discovers the authorization, token, user info, and signing-key endpoints. ### Google Workspace ### Configure the consent screen Open the [Google Auth Platform](https://console.cloud.google.com/auth/overview) in a Google Cloud project owned by your organization. Set the app audience to **Internal** so only users in your Google Workspace organization can authorize it. ### Create a web OAuth client Create an OAuth client with the application type **Web application**. Add the camelAI callback URL under **Authorized redirect URIs**. You can leave **Authorized JavaScript origins** blank. Google may take a few minutes to apply a newly added redirect URI. ### Enter the connection in camelAI Copy the client ID and client secret, then use: | camelAI field | Value | | ----------------------------- | -------------------------------------------------------------------- | | Issuer URL | `https://accounts.google.com` | | Token endpoint authentication | **Client secret POST** | | Email claim | **email** | | Allowed email domains | Leave blank for the first test, or enter your exact Workspace domain | camelAI requests the `openid`, `email`, and `profile` scopes. It does not request access to Gmail, Drive, or other Workspace data. ### Microsoft Entra ID ### Create a single-tenant app registration In the [Microsoft Entra admin center](https://entra.microsoft.com/), open **Identity → Applications → App registrations** and select **New registration**. Choose **Accounts in this organizational directory only**. Under **Redirect URI**, choose **Web** and enter the camelAI callback URL. ### Create the client secret Open **Certificates & secrets**, create a client secret, and copy its **Value** before leaving the page. From **Overview**, also copy the **Application (client) ID** and **Directory (tenant) ID**. ### Enter the connection in camelAI Use: | camelAI field | Value | | ----------------------------- | ------------------------------------------------------------------ | | Issuer URL | `https://login.microsoftonline.com//v2.0` | | Client ID | Application (client) ID | | Client secret | Client secret **Value** | | Token endpoint authentication | **Client secret POST** | | Email claim | **preferred\_username** | | Allowed email domains | Leave blank for the first test, or enter your exact company domain | Use your tenant ID in the issuer. Do not use the multitenant `common` or `organizations` endpoints. ### Limit access in Entra In **Enterprise applications**, open the application and assign the users or groups that should be able to use camelAI. If assignment is required, confirm your test user is assigned. See [Microsoft's OIDC reference](https://learn.microsoft.com/en-us/entra/identity-platform/v2-protocols-oidc) for the tenant-specific discovery format. ### Okta ### Create an OIDC web application In the Okta Admin Console, open **Applications → Applications** and select **Create App Integration**. Choose **OIDC - OpenID Connect**, then **Web Application**. Keep **Authorization Code** enabled and add the camelAI callback URL as a sign-in redirect URI. ### Assign users and copy credentials Assign the people or groups that should use camelAI. From the application's **General** tab, copy the client ID and client secret. ### Enter the connection in camelAI For the simplest SSO setup, use Okta's organization authorization server: | camelAI field | Value | | ----------------------------- | ------------------------------------------------------------------ | | Issuer URL | `https://` | | Token endpoint authentication | **Client secret Basic** | | Email claim | **email** | | Allowed email domains | Leave blank for the first test, or enter your exact company domain | If your organization already uses a custom authorization server, enter its exact issuer instead, such as `https:///oauth2/default`. Confirm it has an access policy that allows this application. See Okta's guides for [Authorization Code web applications](https://developer.okta.com/docs/guides/implement-grant-type/authcode/main/) and [authorization servers](https://developer.okta.com/docs/concepts/auth-servers/). ### Auth0 ### Create a regular web application In the Auth0 Dashboard, open **Applications → Applications** and select **Create Application**. Choose **Regular Web Applications**. ### Configure the callback and credentials In **Settings**, add the camelAI callback URL to **Allowed Callback URLs**. In **Credentials**, set **Token Endpoint Authentication Method** to **Post**. Copy the application's domain, client ID, and client secret. Make sure the connections your team uses are enabled for the application. ### Enter the connection in camelAI Use: | camelAI field | Value | | ----------------------------- | ----------------------------------------------------------------------- | | Issuer URL | Your exact Auth0 issuer, such as `https://..auth0.com/` | | Token endpoint authentication | **Client secret POST** | | Email claim | **email** | | Allowed email domains | Leave blank for the first test, or enter your exact company domain | Keep the trailing slash in the Auth0 issuer. If you use an Auth0 custom domain, use that domain consistently for the issuer and application configuration. See Auth0's guides for [regular web applications](https://auth0.com/docs/get-started/auth0-overview/create-applications/regular-web-apps) and [application settings](https://auth0.com/docs/get-started/applications/application-settings). ### Custom OIDC Your provider must publish a standard discovery document from: ```text /.well-known/openid-configuration ``` The document must include HTTPS authorization, token, and JWKS endpoints. Create a confidential web application in your provider, register the callback URL shown in camelAI, and configure it to return a stable `sub` identifier plus an `email` or `preferred_username` claim. Enter the issuer URL, client credentials, matching token endpoint authentication method, and email claim in camelAI. > Enter the provider's exact issuer value, not the discovery document URL. > For example, enter `https://accounts.google.com`, not a URL ending in > `/.well-known/openid-configuration`. ## Test and enable SSO ### Test the connection Click **Test connection** and complete the provider login with a regular organization user. camelAI checks the discovery document, authorization and token endpoints, signing keys, token exchange, and returned email. Google Workspace tests also verify the email and hosted domain. The test does not create a user, link an account, or enable SSO. ### Enable SSO When every connection check passes, click **Enable SSO**. camelAI shows an organization-specific sign-in URL. Test it in a private browser window before sharing it with your team or adding it to your internal app launcher. ## Allowed email domains Allowed domains are an additional camelAI-side boundary. They protect the organization if the identity provider application is accidentally assigned too broadly or its policy changes later. Enter domains without `@`, for example: ```text example.com ``` For Google Workspace, camelAI also requires a verified email and a matching Google hosted-domain (`hd`) claim. ## Add and remove users ### Add a user If uninvited users are not allowed: 1. Invite the exact email address from your camelAI Team settings. 2. Choose the user's organization role and workspace access. 3. Send the user your organization-specific SSO sign-in URL. The first successful SSO login creates the account, consumes the invitation, applies its access, and signs the user in. If uninvited users are allowed, permit the user in your identity provider and send them the same SSO sign-in URL. No camelAI invitation is required. ### Remove a user Remove the member from the camelAI organization and disable or unassign them in your identity provider. Removing the camelAI membership revokes that organization's SSO access even if the provider still authenticates the user. To restore a removed user, send a new invitation to the exact same email. Their next SSO login restores the membership with the new invitation's role and workspace access. ## Troubleshooting ### Could not validate the OIDC issuer and discovery document Confirm that the issuer is an HTTPS URL and matches the provider's issuer exactly. Do not paste the discovery document URL itself. Open `/.well-known/openid-configuration` in a browser and confirm it returns JSON. Its issuer must exactly match the value entered in camelAI, and its authorization, token, and JWKS endpoints must be valid HTTPS URLs. ### Google reports redirect\_uri\_mismatch Confirm that the callback URL shown in camelAI appears exactly under **Authorized redirect URIs** on the correct Google **Web application** client. Check the scheme, hostname, path, and trailing slash. If you just added the URI, wait a few minutes and try again. ### No organization member or active invitation matches this enterprise identity Uninvited SSO users are not allowed, and camelAI could not find an active invitation for the exact email returned by the provider. Send a new invitation to that email, or turn on **Allow uninvited SSO users** if everyone permitted by the identity provider should be able to join. ### This email is already bound to a different enterprise identity The email was previously linked to a different provider subject or SSO connection. camelAI blocks automatic rebinding to protect the existing account. Sign in normally and use the explicit account-link flow, or contact [support@camelai.com](mailto:support@camelai.com). ### Google Workspace did not verify this email and hosted domain Use a managed Google Workspace account, confirm its email is verified, and check that the Google hosted domain matches your camelAI allowed-domain configuration. ## Security notes * camelAI binds each login to the provider's stable OIDC subject, not email alone. * The provider must return the configured email claim. Google Workspace logins must also include a verified email and matching hosted domain. * Invitation-gated account creation requires an active invitation for the exact asserted email. * New SSO-created accounts are scoped to the organization. * SSO sessions are restricted to the organization that issued them. * Client secrets are encrypted at rest and are never shown again after saving. --- --- title: 'Your Workspace' description: 'A persistent computer that never resets' --- Your workspace is a real Linux computer with 100GB of storage. Files, databases, and projects survive across sessions. It's not a sandbox that resets — it's your agent's permanent home. ## What's in a workspace ### Files & folders Browse, upload, and download files through the built-in Computer tab. Code gets syntax highlighting, notebooks render beautifully, and images preview inline. ### Multiple projects One workspace can hold many projects. Build a dashboard, a landing page, and an internal tool — all in the same place. ### Connections Each workspace has its own set of integrations — databases, SaaS tools, and APIs that the agent can use in any project. ### Chat history Every conversation is saved and resumable. Come back to any thread and pick up where you left off. ## The Computer tab The Computer tab is your file browser. From here you can: * Browse your workspace filesystem * Preview source code with syntax highlighting * View rendered notebooks, charts, and data tables * Preview images and markdown files * Upload and download files ## Multiple chats, one computer This is the key difference between camelAI and other AI coding tools. camelAI is **computer-based, not project-based.** Your workspace is the container — chats are just conversations about what's in it. * Start a new chat anytime without losing your work * Work on different projects from different chats * If a conversation goes sideways, start fresh — your files are still there > **Tip:** > > Think of chats as conversations, not containers. You can always start a new chat > and say "look at the project in /my-app and fix the login page" — the agent will > find it. ## Workspaces and organizations See [Organizations and workspaces](/docs/getting-started/organizations) for how workspaces fit into the org structure. ## Idle workspaces ### What happens when the workspace is idle? Your workspace container sleeps after a period of inactivity to save resources. When you return, it wakes up automatically — you may notice a brief startup delay. Your files are always preserved regardless of container state. --- --- title: 'Your Apps' description: 'How live apps work on camelAI' --- When the agent builds an app, it's automatically deployed to a live URL. There's no publish button, no deploy pipeline, no CI/CD to configure. Your app is on the internet as soon as it's built — and updates go live as you iterate. ## Your app URL Every app gets a URL at: ``` {app-name}-{your-org}.camelai.app ``` For example: `my-dashboard-k7m2p3.camelai.app` This is a permanent, shareable link. Copy it and send it to anyone. ## Auto-deploy You don't need to do anything to deploy. As the agent builds and updates your app, changes go live automatically. Just keep chatting: > Add a dark mode toggle to the dashboard The agent makes the change, and it's live. ## Public vs. private * **Public** — anyone with the link can access your app * **Private** — only your workspace members can access it (requires authentication) You can toggle visibility anytime from the **Apps** section in workspace settings. ## Managing your apps Go to **Settings → Apps** in your workspace to: * View all your live apps with their URLs * Toggle public/private visibility * Delete apps you no longer need To update an app, just keep chatting with the agent — ask for changes and they go live automatically. ## Under the hood ### Technical details * Apps run as [Cloudflare Workers](https://workers.cloudflare.com) — zero cold start, globally distributed on Cloudflare's edge network * Every app has a **full backend by default** — not just a static frontend. Your apps can have API routes, server-side logic, and database access from the first line of code * Integration credentials from your workspace connections are automatically injected as environment variables — no manual config needed * AI bindings are available in every deployed app, so your apps can use text models, image generation, and other AI features without managing API keys ### Custom domains Bring your own domain and serve apps from it. Available on Starter, Pro, Team, and Enterprise plans — see [plans and pricing](/docs/plans/overview) for per-plan limits. Configure under **Settings → Domains** in your workspace. > **Info:** > > Apps stay live even when you're not in camelAI. They're real production services, > not previews. --- --- title: 'Connections' description: 'Connect to 50+ databases and tools' --- Connections let you plug your existing tools into camelAI. Connect a database, a SaaS platform, or a cloud provider — then build apps that read from and write to them. No API plumbing required. ## Adding a connection You can add connections two ways: ### Through Settings ### Open workspace settings Go to **Settings → Connections** in your workspace. ### Pick a service Choose from the catalog of 50+ integrations. ### Authenticate Enter credentials or sign in via OAuth, depending on the service. ### Through the agent Just ask the agent in any chat: > Connect my Stripe account or > I want to pull data from our Snowflake warehouse The agent will walk you through setup — it may open a credential entry form for you, or handle the configuration directly. ## How connections work * **Encrypted storage** — credentials are encrypted at rest and never exposed in logs or chat * **Auto-injected** — once connected, the integration is available to the agent and to your deployed apps' server-side code through a secure connection binding. camelAI adds the credentials when your app calls out, so raw keys never appear in your code, logs, or chat * **Any chat, any project** — once connected, the agent can use the integration from any conversation in your workspace ## Network access camelAI connects to your databases from one fixed IP address: ``` 20.46.233.68 ``` If your database only accepts connections from approved addresses, for example because it sits behind a firewall or inside a VPC, add this address to its allowlist. camelAI shows it when you set up PostgreSQL, MySQL, Snowflake, ClickHouse, MongoDB, or Redis, and on the connection's details afterward. ## Available integrations ### Databases PostgreSQL, MySQL, Supabase, MongoDB, Redis, ClickHouse, Snowflake, BigQuery, Neon, PlanetScale, Turso, Databricks, SingleStore, MotherduckDB ### SaaS & Productivity Notion, Airtable, HubSpot, Salesforce, Jira, Zendesk, Intercom, Asana, Figma, Shopify, Typeform ### Communication Slack, Discord, Microsoft Teams, Twilio, SendGrid, Mailchimp, [Resend](/docs/getting-started/partners/resend) ### Payments Stripe, Square ### Developer Tools GitHub, Linear, Sentry, Vercel, Netlify ### Cloud & Analytics AWS, Google Cloud, Microsoft Azure, Cloudflare, PostHog, Mixpanel, Segment, Amplitude ### AI Services OpenAI, Anthropic, OpenRouter > These connections let apps the agent builds call AI models. They don't > power the agent itself — for that, see [Model Providers](/docs/plans/model-providers). ### Custom API Connect to **any HTTP API** with bearer, basic, or custom authentication. Define up to 10 credential fields. The agent will guide you through setup. ## Using connections in your apps Once connected, just describe what you want in plain English: > Pull my last 30 days of Stripe charges and show them in a sortable table > Every morning, query our Snowflake warehouse for yesterday's signups and email > me a summary > Build a dashboard that shows our PostHog events alongside Stripe revenue The agent handles the code. Your credentials are already available — no config files, no `.env` management, no API key juggling. > **Tip:** > > You don't need to know how the API works. Just describe what you want and the > agent figures out the integration details. --- --- title: 'Resend' description: 'Send email from your camelAI apps, workflows, and agent with Resend' --- [Resend](https://resend.com) is an email API for sending transactional and marketing email. Connect it to camelAI — the agent that builds and deploys your apps — and the agent, your scheduled workflows, and your deployed apps can all send email through your own Resend account. You describe what you want; the agent writes the code and uses your connection to send it. > This connection is for email **your** apps and workflows send. It isn't used for > camelAI's own platform emails (invites, password resets), and it's separate from the > [workspace email inbox](/docs/features/email-and-slack) you use to talk to the agent. ## What you can build Describe any of these in a chat and the agent builds it against your Resend connection: > When a new row lands in our Stripe connection, send the customer an order > confirmation email with Resend. > Every night, check for records pending review longer than 30 days and email a > summary to [dev-alerts@company.com](mailto:dev-alerts@company.com) via Resend. > After someone signs up in our React app, send them a welcome email through Resend. > Query our active subscriber table and send this newsletter to everyone with Resend. The prompt is the work. The agent writes the sending code, wires it to the right trigger, and uses your connection for credentials. ## Connect Resend ### Create a Resend API key In your [Resend dashboard](https://resend.com), go to **API Keys → Create API Key**. Give it a name, and assign the **Sending Access** permission (choose **Full Access** only if your app needs Resend's read or admin endpoints). Copy the key — it starts with `re_`. ### Add the connection in camelAI You can do this two ways: ### Through Connections Open **Connections** in your workspace and click **Add connection**. Search for **Resend** and select it. Give the connection a name (for example `Resend Production`), paste your `re_...` key into the **API Key** field, and click **Create Connection**. ### Through the agent Ask the agent in any chat: > Connect my Resend account It opens the credential form for you to paste your API key. ## Use it The simplest path is to not write any code. Ask the agent: > Email me a summary of yesterday's signups every morning at 8am via Resend. The agent builds the app or [scheduled workflow](/docs/features/cron-jobs) and sends through your connection. If you're writing the code yourself, resolve the connection by type and call its `fetch` method. Don't hard-code an alias — camelAI derives it from the connection's type and name. ```typescript Deployed app (server code) import { createConnections } from "~/lib/connections"; export async function action({ context }) { const connections = createConnections(context.cloudflare.env); const resend = await context.cloudflare.env.CONNECTIONS.find({ type: "resend" }); const res = await connections[resend.alias].fetch("/emails", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from: "Onboarding ", to: ["recipient@example.com"], subject: "Welcome aboard!", html: "

Thanks for signing up!

", }), }); if (!res.ok) { throw new Error(`Resend failed: ${res.status} ${await res.text()}`); } return await res.json(); } ``` ```javascript Agent workflow const resend = await env.CONNECTIONS.find({ type: "resend" }); const res = await connections[resend.alias].fetch("/emails", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ from: "Onboarding ", to: ["recipient@example.com"], subject: "Welcome aboard!", html: "

Thanks for signing up!

", }), }); return { status: res.status, body: await res.text() }; ``` > **Warning:** > > Connection calls run **server-side only** — in loaders and actions, the agent's > JavaScript runtime, or scheduled workflows. Never call a connection from browser/client > code. Your API key is never exposed to app code, logs, or chat; camelAI injects the > Bearer token at the gateway. ## How it works * **Native Communication connection** — Resend is built in, with a fixed base URL and a single API key field. No base URL or auth type to set up. * **Encrypted storage** — your key is encrypted at rest. Connection listings show only that credentials exist, never the key itself. * **Authenticated proxy** — calls go through an authenticated `fetch` to `https://api.resend.com`. Relative paths resolve against that base; absolute URLs are restricted to that origin. camelAI adds the `Authorization: Bearer` header server-side. * **Server-side execution** — the connection is available to the agent, workflows, and your apps' server code, never to client code. ## Staging and production You can add more than one Resend connection to a workspace — for example one named `Production` and one named `Staging` — and resolve the one you want by name: ```javascript const resend = await env.CONNECTIONS.find({ type: "resend", name: "Production" }); ``` Because your code resolves the connection by type and name rather than by an embedded key, rotating a key in Resend doesn't require any code changes. ## FAQ ### Does camelAI charge for sending email? No. Resend bills your email volume under your own Resend account. camelAI doesn't charge per email or spend credits when you send through your connection. ### Can I use multiple keys or environments? Yes. Add a separate named connection for each key — for example `Production` and `Staging` — and resolve the one you want with `CONNECTIONS.find({ type: "resend", name: "Production" })`. ### What happens when I rotate my Resend key? Update the API key on the connection. Your app code doesn't change, because it resolves the connection by type and name rather than by the embedded key. ### My emails aren't sending. What should I check? 1. Check your app logs for the response status from the Resend call. 2. Confirm the `from` domain is verified in your Resend dashboard. 3. Check the Resend dashboard for rate limits or account restrictions. ## Stop using or remove Resend There's no per-app "unlink" — connections are scoped to a workspace, not granted app by app. To stop one app from sending email, change or remove the code that calls the connection. To cut off everything, either rotate the API key in Resend (then update the connection) or delete the connection: ### Open Connections Go to **Connections** in your workspace and find the Resend connection. ### Delete it Open the connection's actions menu, choose **Delete**, and confirm **Delete connection**. > **Warning:** > > Deletion is permanent. Any app code or workflow that still resolves this connection > will fail until you update the code or create a replacement. --- --- title: 'Plans & Pricing' description: "What each plan includes, how credits work, and the four ways to unlock premium models." --- camelAI is free to start. Sign up and you can chat with the agent right away on **Camel Free**, our hosted model that costs nothing to use. No credit card required. Paid plans raise the platform limits (deployed apps, automations, storage, seats) and include monthly credits for premium models like Claude and GPT. You can also unlock premium models without subscribing at all; see [Unlocking premium models](#unlocking-premium-models) below. ## Plans ### Free **$0 / month** Chat on Camel Free and deploy up to 3 apps. No credit card required. ### Starter **$10 / month** For solo builders. Includes $10 of premium model credits every month. ### Pro **$40 / month** · *Most popular* For power users. Includes $40 of premium model credits every month. ### Team **$50 / seat / month** · 3 seat minimum For teams. Includes $50 of credits per seat every month. ### [Enterprise](https://book-demo--camelai-team-d9e.camelai.app/) **Custom** For larger teams or anything custom. SSO, BYOC, HIPAA/SOC 2. ### Compare plans | | Free | Starter | Pro | Team | | ------------------------- | --------- | ---------- | ---------------- | --------------------------- | | **Price** | $0 | $10 / mo | $40 / mo | $50 / seat / mo | | **Premium model credits** | - | $10 / mo | $40 / mo | $50 / seat / mo | | **Camel Free model** | Included | Priority | Priority | Priority | | **Seats** | 1 | 1 | 1 | 3+ | | **Workspaces** | 1 | 1 | 1 | 2 | | **Storage per workspace** | 5 GB | 50 GB | 100 GB | 100 GB | | **Deployed apps** | 3 | 30 | Unlimited | Unlimited | | **Custom domains** | - | 10 | Unlimited | Unlimited | | **Automations** | 1 · daily | 1 · hourly | 50 · every 5 min | 50 per member · every 5 min | | **Email inbox** | - | ✓ | ✓ | ✓ | | **Role-based access** | - | - | - | ✓ | Deployed app and automation limits are per workspace, except Team automations, which are counted per member. The time next to automations is how often each one can run. See [Organizations and workspaces](/docs/getting-started/organizations) for how workspaces relate to your account, and [Automations](/docs/features/cron-jobs) for what they can do. **Enterprise** removes the limits above and adds SSO / SAML, bring your own cloud (deploy apps inside your own Cloudflare account), HIPAA / SOC 2, and dedicated support. [Contact sales](https://book-demo--camelai-team-d9e.camelai.app/) to talk through your requirements. ## Unlocking premium models Every plan includes Camel Free, and it handles everyday tasks well. It runs on shared capacity, so it can slow down at busy times. Paid plans get priority over free traffic. Premium models (Claude, GPT, Gemini, and more) cost money to run. camelAI passes that cost through at the provider's rate with no markup. There are four ways to cover it, and all four work on any plan, including Free: ### Subscribe to a plan Starter, Pro, and Team come with monthly credits matching what you pay: $10, $40, and $50 per seat. You get fresh credits every month, and they cover every model camelAI hosts, so you can switch freely between Claude, GPT, Gemini, and the rest. Subscribing also raises your platform limits. ### Top up credits Buy a prepaid credit pack any time from **Settings → Usage**, with or without a subscription. 1 credit = $1, usage is billed at the provider's cost, and top-up credits never expire. This is the way to use premium models on the Free plan without subscribing. ### Bring your own API key Add a key from Anthropic, OpenAI, OpenRouter, AWS Bedrock, or a custom endpoint under **Settings → AI Provider**. Your provider bills you directly, and your model list comes from whatever that provider serves. See [Model Providers](/docs/plans/model-providers) for which provider unlocks which models. ### Sign in with OpenAI Already pay for ChatGPT? Connect your OpenAI account with a short sign-in flow (no API key to copy) and GPT models run on the allowance included in your ChatGPT plan, at no extra cost. If you have a ChatGPT subscription, this is by far the cheapest way to use GPT models on camelAI. ### How these fit together * Plan credits and top-up credits share one balance, and usage draws from it no matter where the credits came from. * An API key replaces camelAI's hosted models entirely, including Camel Free. All requests go through your one chosen provider. If you want broad model coverage on a single key, use [OpenRouter](/docs/plans/model-providers). * Sign in with OpenAI is the exception: it works alongside any other setup and only takes over GPT models. ### If you run out of credits Your chat keeps working. You can continue on Camel Free, and your apps, automations, and workspace are untouched. To get back on a premium model, top up, upgrade your plan, add an API key, or sign in with OpenAI. Subscribers get fresh credits at the start of the next billing cycle. ## Topping up credits ### Open usage settings Open **Settings → Usage** in your workspace. ### Top up credits Click **Top up credits.** ### Check out Pick a credit pack and check out through Stripe. Available pack sizes are shown in the dialog. ## Why prepaid Credits are paid for up front, the same way OpenAI, Anthropic, and OpenRouter sell them. You only spend what you put in, and we only run requests against funded accounts, which keeps fraud and runaway bills off the table for both sides. The monthly platform fee is billed normally on a recurring basis through Stripe. ## Changing plans Upgrade, downgrade, or cancel anytime from **Settings → Billing.** Plan changes go through the Stripe billing portal. --- --- title: 'Model Providers' description: 'How to use your own API key with camelAI, and what each provider unlocks.' --- camelAI is model-agnostic. By default, camelAI's hosted credits (included with paid plans, or topped up on any plan) call whichever model you've picked for the thread, and the Camel Free model is included on every plan with no credits or key at all. If you'd rather use your own API key, you can bring one instead. This page explains what each option gives you and how to set it up. The agent can run on the major model families camelAI supports: GPT, Gemini, Grok, Kimi, DeepSeek, and Claude. You can switch models any time from the model picker in the chat. Whichever provider you've connected determines which models are available to you. > **Warning:** > > Bringing your own key replaces camelAI's hosted models entirely, including Camel Free. You can't mix and match your own key for one model family with camelAI credits for another. All requests go through your one chosen provider. If you need access to multiple model families on a single key, use **OpenRouter.** The one exception is **Sign in with OpenAI** (below), which works alongside any setup for GPT models. ## When to use camelAI credits * You don't already have a provider account. * You want one bill, not multiple. * You want every supported model available without managing separate keys. ## When to bring your own key * You're on the Free plan and would rather use your provider account than prepay camelAI credits. * You have provider credits you want to make use of. * Your company needs usage on a corporate AWS or Azure bill for compliance or finance reasons. * You need a provider we don't support natively (Microsoft Azure, Google Vertex, etc.). See [Using an unsupported provider through OpenRouter](#using-an-unsupported-provider-through-openrouter) below. ## Supported providers camelAI supports four providers for BYOK. Each unlocks a specific set of models. ### Anthropic: Claude models **Unlocks:** Claude models, including Opus, Sonnet, and Haiku. **Get a key:** [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys) You'll need to add a payment method and prepay for credits in the Anthropic console before your key will run requests. A new key with no credits behind it returns a billing error. ### OpenAI: GPT models **Unlocks:** GPT models, including GPT-5.6 Sol, GPT-5.6 Terra, and GPT-5.5. **Get a key:** [platform.openai.com/api-keys](https://platform.openai.com/api-keys) Like Anthropic, OpenAI requires you to prepay credits in your OpenAI account before the key will run requests. ### OpenRouter: every model camelAI supports, on one key **Unlocks:** GPT, Gemini, Grok, Kimi, DeepSeek, and Claude models, all on a single key. **Get a key:** [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys) OpenRouter is the most flexible BYOK option. Two reasons to pick it: 1. You want every model camelAI supports on one key. Anthropic, OpenAI, and Bedrock keys lock you into a subset of our models. OpenRouter gives you all of them. 2. You already have an account with a provider we don't connect to natively. OpenRouter supports plugging in your own keys for Microsoft Azure, AWS Bedrock, Google Vertex, and others, then routing requests through your account. So if your company runs AI on Azure OpenAI, you can connect your Azure key to OpenRouter, and then connect OpenRouter to camelAI. Usage runs against your Azure bill. OpenRouter charges a 5% fee on requests routed through your own provider keys, but the first 1 million BYOK requests per month are free. See [OpenRouter's BYOK guide](https://openrouter.ai/docs/use-cases/byok) for details. ### AWS Bedrock: models served from your AWS account **Unlocks:** Claude models served from your own AWS account. **Get a key:** [console.aws.amazon.com/bedrock](https://console.aws.amazon.com/bedrock) You'll need: * An AWS access key with Bedrock permissions * The region you want to run in (for example, `us-east-1`) Bedrock is a good fit if your team needs model usage on an AWS bill, or has compliance requirements that route AI through your own AWS account. ## Using an unsupported provider through OpenRouter camelAI doesn't connect natively to Microsoft Azure OpenAI or Google Vertex AI. Each cloud provider has its own auth model, billing structure, and quirks, and supporting them all directly would slow down our work on the rest of the platform. The good news: if your company is on one of those providers, you can still run camelAI through your existing account by routing through **OpenRouter**. ### How it works OpenRouter supports plugging in your own keys for a long list of cloud providers, including: * **Microsoft Azure OpenAI** * **Google Vertex AI** * **AWS Bedrock** (alternative to our direct Bedrock integration) * Others. See [OpenRouter's BYOK guide](https://openrouter.ai/docs/use-cases/byok) for the current list. When you connect those keys to OpenRouter, OpenRouter forwards your requests to that provider. Usage runs against your Azure (or Vertex, or Bedrock) bill, and OpenRouter charges a 5% fee on top, waived for the first 1 million BYOK requests per month. ### Setup with Azure as the example ### Set up Azure OpenAI In your Azure portal: deploy a model, note the endpoint URL, deployment name, and API key. ### Sign up for OpenRouter Sign up at [openrouter.ai](https://openrouter.ai) and go to **Integrations → Bring Your Own Key.** ### Add Azure as a provider in OpenRouter Paste the endpoint URL, model ID, and API key per OpenRouter's [Azure setup instructions](https://openrouter.ai/docs/use-cases/byok). ### Generate an OpenRouter API key Generate a key at [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys). ### Add the OpenRouter key to camelAI Add the key under **Settings → AI Provider → OpenRouter.** The same flow works for Google Vertex (Google Cloud service account JSON) or AWS Bedrock through OpenRouter (AWS access key + region). > Native Azure or Vertex support in camelAI isn't on the near-term roadmap. The OpenRouter pass-through is the recommended path for the foreseeable future, and it's how most of our customers in this situation are running today. ## Adding a key ### Open AI Provider settings Open **Settings → AI Provider** in your workspace. ### Pick a provider Choose Anthropic, OpenAI, OpenRouter, or AWS Bedrock. ### Paste your API key Paste your API key (and select a region, if you're using Bedrock). ### Save Save the form. The form has a "Get a key" link next to each provider that takes you to the right page on the provider's site. You can switch providers any time from the same screen, or remove your key and switch to camelAI hosted credits (included with paid plans, or topped up on any plan). ## Sign in with OpenAI If you have a ChatGPT subscription, you can connect it under **Settings → AI Provider** with a short sign-in flow. There's no API key to copy. GPT model usage then runs against the allowance already included in your ChatGPT plan, at no extra cost. Two things make this different from a regular API key: * It covers **GPT models only**. * It **combines** with everything else. Unlike an API key, it doesn't replace your other setup. Keep using camelAI credits or another provider's key for non-GPT models, and your connected OpenAI account handles GPT traffic. If you already pay for ChatGPT, this is by far the most cost-effective way to run GPT models on camelAI. ## Using camelAI on the cheap > **Tip:** > > The cheapest model on camelAI is **Camel Free**, included on every plan at no cost (see [Plans & Pricing](/docs/plans/overview)). Beyond that, model costs vary widely: the cheapest premium models are 10 to 50 times less expensive per request than the flagships, and they handle most everyday work just as well. Pairing a cheaper model with a small OpenRouter top-up is the lowest-cost way to run premium models. The playbook: ### Sign up for OpenRouter Sign up and add the minimum in credits at [openrouter.ai/credits](https://openrouter.ai/credits). A small top-up goes a long way with cheap models. ### Generate an OpenRouter API key Generate a key at [openrouter.ai/settings/keys](https://openrouter.ai/settings/keys). ### Add the key to camelAI Add the key under **Settings → AI Provider → OpenRouter.** ### Pick a cheap model in the chat The most cost-efficient options are: * **DeepSeek V4 Flash:** a low-cost option for straightforward coding tasks. * **DeepSeek V4 Pro:** stronger reasoning at a still-low price. * **Haiku 4.5:** fast and cost-effective for everyday code and copy work. * **Gemini 3 Flash Preview:** fast, low-cost, and useful for general iteration. * **Kimi K2.7 Code:** competitive on price, particularly strong on long-context tasks. ### Reserve flagship models for hard problems Switch to a flagship model only when a task is genuinely hard (complex reasoning, multi-step debugging, careful design work). Most chat turns don't need one. OpenRouter shows the live per-token cost for every model on its [models page](https://openrouter.ai/models). Sort by price to compare. ## FAQ ### Can I use my own API key for one model and camelAI credits for another? No. When a BYOK key is set, all of your model usage runs through that one provider. If you want broad model coverage on a single key, use OpenRouter. The one exception is **Sign in with OpenAI**, which layers on top of any setup and covers GPT models only. ### Does my API key work across multiple orgs? No. LLM provider keys are scoped to one org. If you have more than one org, set up a key for each. See [Organizations and workspaces](/docs/getting-started/organizations) for the full breakdown. ### Do my credits expire? Top-up credits never expire. Plan credits arrive fresh every month with your subscription. ### Why does my new API key say "billing error" or "insufficient credits"? Anthropic, OpenAI, and OpenRouter all let you create an API key without adding any credits. The key won't work until you prepay credits in their billing settings. This is on the provider's side, not ours. ### Does camelAI charge a markup on hosted credits? No. We pass through the provider rates at cost. The monthly subscription covers the platform; credits cover model usage at the provider's rate. ### Can I see how much I've spent? Yes. **Settings → Billing** shows your current plan, credit balance, and usage history. ### Do you support Microsoft Azure OpenAI or Google Vertex AI? Not directly, but you can route through OpenRouter to use them. See [Using an unsupported provider through OpenRouter](#using-an-unsupported-provider-through-openrouter) above for the setup walk-through. --- --- title: 'Cron Jobs' description: 'Schedule your agent to work autonomously' --- Tell your agent what to do and when — it sets up a recurring job and runs it without any user interaction. Each run is a full agent turn with access to your workspace, files, and connections. ## Setting up a cron job Just describe the schedule in natural language: > Every weekday at 8am, pull yesterday's revenue from Stripe, generate a summary, > and email it to the team. or > Every 15 minutes, check if there are new sign-ups in our database and post them > to the #growth Slack channel. The agent translates this to a cron schedule and sets it up. You never need to write cron expressions. ## What happens on each run Each scheduled job gets its own dedicated chat thread (named `Scheduled: {job name}`) that appears alongside your regular chats. When a job fires: * The agent runs with **full access** to your workspace, files, and connections — the same as if you had typed the message yourself * **Context accumulates** across runs — the agent remembers what it did last time, can compare against previous results, and builds up a history of its work * Results can be emailed, posted to Slack, saved to files, or published to a live dashboard > **Info:** > > Cron jobs are reliable — they're backed by Cloudflare Durable Object alarms and > survive even when your workspace container is idle. ## Example use cases ### Daily reports Pull data from your warehouse every morning, generate a formatted report, and email it to stakeholders ### Monitoring & alerts Check metrics hourly, flag anomalies, and page your team when something looks off ### Data syncing Keep two systems in sync — new Stripe customers automatically become HubSpot contacts every 15 minutes ### Content generation Generate weekly newsletter drafts, daily social media posts, or morning news roundups on a schedule ## Managing cron jobs Everything is managed through chat. Ask the agent: * **"List my scheduled jobs"** — see all active cron jobs * **"Update the daily report to run at 9am instead"** — modify a schedule or prompt * **"Delete the Stripe sync job"** — remove a scheduled job * **"Run the daily report now"** — trigger a job immediately without waiting for the next scheduled time > **Tip:** > > You can view the execution history of any cron job by opening its dedicated thread > in your chat history. Each run is logged as a message in the thread. --- --- title: 'Email & Slack' description: 'Talk to your agent from your inbox or Slack' --- You don't have to be in camelAI to talk to your agent. Send an email or a Slack message — it reads, responds, and takes action on your workspace, just like a normal chat. > This page is about talking *to* the agent over email. To send email *from* your apps > and workflows, connect [Resend](/docs/getting-started/partners/resend). ### Email ## Workspace email Every workspace gets a unique email address like: ``` bright-mountain-river@camelai.dev ``` Any workspace member can email this address to start or continue a conversation with the agent — directly from their inbox, without opening camelAI. ### How it works ### Find your workspace email Go to **Settings → General** in your workspace, or just ask the agent: "What's this workspace's email address?" ### Send an email Email the workspace address from your work email. Write your request in the body — the agent processes it as a chat message. ### Get a reply The agent reads your email, takes action in your workspace, and replies to your email thread. Reply back to continue the conversation. ### Details * **Threading** — standard email threading keeps conversations organized in your email client. Replies continue the same camelAI chat thread. * **Attachments** — file attachments are automatically uploaded to your workspace. The agent can read and use them immediately. * **Members only** — only workspace members can email the inbox. Emails from non-members are ignored (this is also the spam prevention mechanism). ### Slack ## Slack integration Connect Slack to your workspace and message the agent directly from your team's Slack. ### Setup ### Add the Slack connection Go to **Settings → Connections** and add the Slack integration, or ask the agent to set it up. ### Authenticate via OAuth Sign in to your Slack workspace and authorize the camelAI bot. ### Start chatting DM the camelAI bot or mention it in a channel. The agent responds in-thread. ### Details * **Full agent turns** — Slack messages are processed the same as web chat messages. The agent can read files, run code, query databases, and take any action it could take from the web app. * **Thread continuity** — conversations are maintained across messages in the same Slack thread. > The Slack integration is **inbound-only** — the agent responds to your messages > but doesn't proactively send messages to Slack. For automated Slack posting, use > a [cron job](/docs/features/cron-jobs) with a Slack connection. > **Tip:** > > Email and Slack are great for quick asks without context-switching — "what were > yesterday's numbers?" or "update the landing page copy to mention the sale." --- --- title: 'Self-host camelCode' description: 'Run camelAI on a single Linux VM with Docker Compose, your identity provider, and your AI provider.' sidebarTitle: 'Overview' --- camelAI supports a single-node deployment for organizations that need to run the control plane and project data in their own environment. The supported topology is one x86\_64 Linux VM running Docker Engine and Docker Compose. The deployment includes the camelAI application, local project storage, Pomerium authentication, and isolated containers for builds, notebooks, and database queries. ### [Install with Docker Compose](/docs/self-hosting/docker-compose) Configure and start camelAI on an existing Linux VM. ### [Deploy on AWS](/docs/self-hosting/aws) Provision a single EC2 node with Terraform or CloudFormation. ## Install with a coding agent In a shell on the target VM (or with SSH access), paste this prompt into a coding agent such as Codex, Claude Code, or OpenCode: ```text Read the canonical installation instructions at https://camelai.com/docs/self-hosting/overview and every linked guide relevant to the installation, then install and configure self-hosted camelAI on this VM. ``` ## Supported topology The self-hosted application runs under local `workerd`. It stores application state and project Git repositories in Docker volumes. When a user builds an app, runs a notebook, or queries a database, `workerd` starts a dedicated Docker container for that job. | Component | Implementation | | -------------------------- | ------------------------------------------------------------------- | | Web application and APIs | camelAI application container running local `workerd` | | Authentication | Bundled Pomerium with OIDC, existing Pomerium, or Cloudflare Access | | Application state | Local Durable Objects, KV, R2, D1, queues, and workflows | | Project source and history | Local Artifact repositories in a persistent Docker volume | | Project builds and deploys | Attached project-build containers | | Notebook analysis | Attached Jupyter analysis containers | | SQL queries and exports | Attached database-query containers | | Published apps | Local dispatcher served through your wildcard app domain | Publishing generated apps does not require a Cloudflare account or Cloudflare Workers API token. A Cloudflare DNS token is optional and is used only when you choose Cloudflare DNS for automatic TLS certificate validation. This target does not provide multi-node failover or a managed control plane. Backups, VM availability, network policy, monitoring, and disaster recovery remain the operator's responsibility. ## Requirements Use a VM with: * x86\_64 Ubuntu 24.04 or another current x86\_64 Linux distribution * At least 4 vCPUs and 8 GiB allocated RAM * 100 GiB of persistent disk recommended * Docker Engine and Docker Compose v2 * Git, Node.js 22, and Bun 1.3.14 * Outbound HTTPS access to GHCR, your AI provider, and package registries You also need: * A hostname for camelAI, such as `camel.example.com` * A separate Pomerium authenticate hostname, such as `authenticate.example.com` * A wildcard domain for deployed apps, such as `*.apps.example.com` * An OIDC application, or an existing Pomerium or Cloudflare Access deployment * An API key for Bedrock, Anthropic, OpenAI, OpenRouter, or a compatible custom provider * Cloudflare or Route 53 DNS-validation access for automatic certificates, an existing enterprise TLS terminator, or a certificate covering the required hostnames Docker may report 7.5 to 7.8 GiB of usable memory on an 8 GiB VM after normal kernel overhead. ## Capability status | Capability | Status | | -------------------------------------------------------------------------- | -------------------------------------------------- | | Chat with Bedrock, Anthropic, OpenAI, OpenRouter, or a compatible provider | Supported with an API key | | Build and deploy generated applications | Supported | | Run notebooks | Supported | | Query connected databases | Supported | | Browser rendering | Supported | | Keep workspace connections unavailable to deployed apps | Supported with `CONNECTIONS_BINDING_ENABLED=false` | | Add organization-specific agent instructions and skills | Supported through `.selfhost/agent/` | | Password signup and email verification | Not supported | | Outbound email | Disabled | | Internal SMTP | Not implemented | | Multi-node failover | Not included | > **Warning:** > > Self-hosted camelAI does not send email. Verification email, invitation > delivery, the email-backed help form, and the agent's email tool are > unavailable. Invitations can still be created so an administrator can > deliver the invitation URL through an approved internal channel. SMTP settings do not enable delivery. Support for an operator-managed internal SMTP server is planned, but no SMTP transport is included today. ## Security boundaries The application container has read-write access to the Docker socket so it can create the isolated execution containers. Anyone who can control that container has root-equivalent control of the VM. Sandbox storage synchronization does not require privileged containers. Do not give generated-code containers `/dev/fuse`, `SYS_ADMIN`, or an unconfined AppArmor profile; current self-host releases synchronize local R2 data through the sandbox file APIs. The application, local Artifact service, and Pomerium upstream bind to loopback by default. Keep them there and expose camelAI only through the Caddy front door and a supported identity-aware proxy. Do not expose the Docker socket over TCP. The bundled Pomerium route protects the camelAI control-plane hostname. Wildcard deployed-app hosts use a separate route so public apps remain public. Private app access is still enforced by camelAI. ## Choose a deployment path Use [Docker Compose](/docs/self-hosting/docker-compose) when you already have a Linux VM and want to manage its network, storage, and TLS directly. Use [AWS single-node deployment](/docs/self-hosting/aws) when you want Terraform or CloudFormation to provision the EC2 instance, encrypted data volume, IAM permissions, Secrets Manager access, DNS, and system services. Read [Authentication and Pomerium](/docs/self-hosting/authentication) before creating DNS or the OIDC application. After installation, use the [operations guide](/docs/self-hosting/operations) for health checks, runtime smokes, backups, and upgrades. Use [agent customization](/docs/self-hosting/agent-customization) to teach camelCode about internal APIs and application standards. --- --- title: 'Install with Docker Compose' description: 'Install camelAI on a single x86_64 Linux VM and verify builds, notebooks, and database queries.' sidebarTitle: 'Docker Compose' --- This guide installs camelAI on an existing Linux VM. The default configuration uses bundled Pomerium, automatic TLS, local persistent volumes, and attached Docker containers for project builds, notebooks, and database queries. ## Before you start Complete the requirements in [Self-host camelAI](/docs/self-hosting/overview). In particular, install Docker Engine, Docker Compose v2, Git, Node.js 22, and Bun 1.3.14. Prepare these values: | Value | Example | | ------------------------------ | ------------------------------------------------ | | camelAI hostname | `camel.example.com` | | Pomerium authenticate hostname | `authenticate.example.com` | | Deployed-app wildcard domain | `apps.example.com` | | OIDC issuer URL | `https://idp.example.com/application/o/camelai/` | | OIDC client ID and secret | Values from your identity provider | | AI provider API key | A supported provider key | | DNS validation credential | Cloudflare API token or Route 53 access | Register this callback URL in your OIDC application: ```text https://authenticate.example.com/oauth2/callback ``` See [Authentication and Pomerium](/docs/self-hosting/authentication) for TLS, identity-provider, and external-proxy options. ## Choose a release Production installations should use a `selfhost-v*` release and all seven digest-pinned camelAI images from that release's `selfhost-release.json` manifest. The manifest also pins the Pomerium image tested with the release. Do not mix images from different releases. > **Warning:** > > If the [camelAI tags page](https://github.com/qaml-ai/camelAI/tags) does not > contain a `selfhost-v*` tag, only the source-build evaluation path below is > available. Do not treat a build from the moving `main` branch as an immutable > production release. Download `selfhost-release.json` from the matching [camelAI GitHub Release](https://github.com/qaml-ai/camelAI/releases). Release assets are permanent and public; do not depend on the expiring Actions artifact. For example: ```bash RELEASE=selfhost-vX.Y.Z curl --fail --location \ --output /secure/path/selfhost-release.json \ "https://github.com/qaml-ai/camelAI/releases/download/${RELEASE}/selfhost-release.json" ``` For a release install, check out the matching tag: ```bash git clone https://github.com/qaml-ai/camelAI.git cd camelAI git checkout --detach selfhost-vX.Y.Z bun install --frozen-lockfile bun run selfhost:init ``` The camelAI release images are public and can be pulled from GHCR without a GitHub account or package token. Set `SELFHOST_DEPLOYMENT_MODE=release` in `.env.selfhost`, then copy all eight digest-pinned image references from the manifest: ```dotenv SELFHOST_DEPLOYMENT_MODE=release SELFHOST_APP_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-app@sha256:... SELFHOST_LOCAL_ARTIFACTS_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-local-artifacts@sha256:... SELFHOST_PROJECT_BUILD_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-project-build@sha256:... SELFHOST_ANALYSIS_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-analysis@sha256:... SELFHOST_DB_QUERY_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-db-query@sha256:... SELFHOST_CONTAINER_EGRESS_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-container-egress@sha256:... SELFHOST_CADDY_IMAGE=ghcr.io/qaml-ai/camelai-selfhost-caddy@sha256:... SELFHOST_POMERIUM_IMAGE=pomerium/pomerium@sha256:... ``` > **Warning:** > > Keep `SELFHOST_CONTAINER_EGRESS_IMAGE` set to the digest from the same > release manifest. Do not replace it with Cloudflare's stock > `cloudflare/proxy-everything` image. The camelAI image includes the Docker > bridge bypass required on affected hosts; the stock sidecar can leave > attached runtimes failing with `Container failed to start`. If you are evaluating unreleased code, clone a tested commit and keep the source mode generated by `selfhost:init`: ```bash git clone https://github.com/qaml-ai/camelAI.git cd camelAI git checkout --detach TESTED_COMMIT_SHA bun install --frozen-lockfile bun run selfhost:init ``` Source mode builds every image on the VM. It takes longer and needs more free disk space than a release install. ## Configure camelAI Edit `.env.selfhost`. `selfhost:init` generated the signing and encryption secrets already. Do not replace them with shared development values. Configure the public domains and bundled Pomerium: ```dotenv SELFHOST_PUBLIC_BASE_URL=https://camel.example.com SELFHOST_MAIN_HOSTNAME=camel.example.com LOCAL_APP_VANITY_DOMAIN=apps.example.com LOCAL_APP_IFRAME_DOMAIN=apps.example.com SELFHOST_AUTH_MODE=bundled-pomerium POMERIUM_AUTHENTICATE_URL=https://authenticate.example.com POMERIUM_AUTHENTICATE_HOSTNAME=authenticate.example.com POMERIUM_ISSUER=camel.example.com POMERIUM_AUDIENCE=camel.example.com POMERIUM_DEFAULT_ORG_NAME=Example Corp POMERIUM_IDP_PROVIDER=oidc POMERIUM_IDP_PROVIDER_URL=https://idp.example.com/application/o/camelai/ POMERIUM_IDP_CLIENT_ID=camelai POMERIUM_IDP_CLIENT_SECRET=replace-with-your-client-secret ``` > `LOCAL_APP_VANITY_DOMAIN` and `LOCAL_APP_IFRAME_DOMAIN` contain the app base > domain without a leading `*.`. With the values above, a deployed app is > served at `https://APP_NAME.apps.example.com`—not at > `APP_NAME.camel.example.com` or directly at `apps.example.com`. Create > the `*.apps.example.com` DNS record and wildcard certificate described below. Choose one AI provider: ### Bedrock camelAI uses a long-term Bedrock API key through the Bedrock Mantle compatibility endpoint. EC2 instance-profile or IMDS SigV4 model authentication is not supported by this deployment. ```dotenv SELFHOST_AI_PROVIDER=bedrock SELFHOST_AI_API_KEY=bedrock-api-key-... SELFHOST_AI_AWS_REGION=us-east-1 ``` `SELFHOST_AI_AWS_REGION` is the preferred region. If Bedrock reports that a selected model is unavailable there, camelAI automatically tries the other supported regions for that model. Authentication and authorization errors are not retried in another region. ### Anthropic ```dotenv SELFHOST_AI_PROVIDER=anthropic SELFHOST_AI_API_KEY=... ``` ### OpenAI ```dotenv SELFHOST_AI_PROVIDER=openai SELFHOST_AI_API_KEY=... ``` ### OpenRouter ```dotenv SELFHOST_AI_PROVIDER=openrouter SELFHOST_AI_API_KEY=... ``` ### Custom endpoint ```dotenv SELFHOST_AI_PROVIDER=custom SELFHOST_AI_API_KEY=... SELFHOST_AI_BASE_URL=https://provider.example.com/v1 SELFHOST_AI_MODEL=provider-model-id SELFHOST_AI_NAME=Provider name SELFHOST_AI_API=openai-completions SELFHOST_AI_AUTH_TYPE=bearer ``` ## Publish generated apps locally Self-hosted camelAI publishes generated apps to its local worker registry and serves them through the local dispatcher on your wildcard app domain. This is automatic: do not configure `CF_API_TOKEN`, `CF_ACCOUNT_ID`, or `CF_DISPATCH_NAMESPACE` for app deployment. The Compose stack fixes the latter two values to the internal self-host mode, so app deployment never calls the Cloudflare Workers API. > `SELFHOST_TLS_CLOUDFLARE_API_TOKEN` is unrelated to app deployment. When you > select Cloudflare DNS for automatic TLS, only Caddy uses that scoped token for > ACME DNS validation and certificate renewal. It can be omitted when you use > Route 53, provided certificates, or an existing TLS proxy. ## Choose whether deployed apps can use connections By default, an app deployed from a workspace can receive an `env.CONNECTIONS` service binding. The binding lets that app list and call the workspace's configured connections. Connection credentials stay inside camelAI, but data returned by a connection can enter the deployed app. For installations where connections should remain available to the coding agent but must not be shared with deployed apps, add this to `.env.selfhost`: ```dotenv CONNECTIONS_BINDING_ENABLED=false ``` > **Warning:** > > This control requires `selfhost-v0.1.2` or later. Set it before the first > `bun run selfhost:configure` and `bun run selfhost:up`. When disabled, deployed-app access through `CONNECTIONS` fails closed. The connection catalog methods exposed through deployed-app `ANALYSIS` and `WAREHOUSE` bindings also fail closed. Connections remain available to the camelCode, `js_exec`, the Connections settings page, and other control-plane workflows. For an existing installation, change the value, run `bun run selfhost:configure`, and restart with `bun run selfhost:up`. Existing apps are blocked immediately by the runtime check. Redeploy them so their saved worker configuration no longer contains the binding. If the agent should build apps against approved internal APIs instead, add deployment-specific instructions with [custom agent skills](/docs/self-hosting/agent-customization). ## Configure TLS and DNS Caddy is the Compose ingress service. It terminates TLS in automatic and provided modes, or exposes a private HTTP origin in external mode. Pomerium remains on the loopback-only `127.0.0.1:5444` upstream. Choose a certificate mode: ### Cloudflare DNS (recommended) ```dotenv SELFHOST_TLS_MODE=automatic SELFHOST_TLS_DNS_PROVIDER=cloudflare SELFHOST_TLS_ACME_EMAIL=platform-ops@example.com SELFHOST_TLS_CLOUDFLARE_API_TOKEN=... ``` Create a Cloudflare API token with Zone Read and DNS Edit on only this DNS zone. Caddy uses it for ACME DNS validation and automatic renewal, including the deployed-app wildcard. ### Route 53 DNS ```dotenv SELFHOST_TLS_MODE=automatic SELFHOST_TLS_DNS_PROVIDER=route53 SELFHOST_TLS_ACME_EMAIL=platform-ops@example.com SELFHOST_TLS_ROUTE53_HOSTED_ZONE_ID=Z0123456789EXAMPLE SELFHOST_TLS_AWS_REGION=us-east-1 ``` On EC2, prefer an instance role. On another VM, set the optional `SELFHOST_TLS_AWS_ACCESS_KEY_ID`, `SELFHOST_TLS_AWS_SECRET_ACCESS_KEY`, and session-token fields. ### Provided PEM ```dotenv SELFHOST_TLS_MODE=provided SELFHOST_TLS_CERTIFICATE_FILE=/secure/path/tls.crt SELFHOST_TLS_PRIVATE_KEY_FILE=/secure/path/tls.key ``` The unencrypted PEM key and certificate chain must cover the main, authenticate, deployed-app wildcard, and any separate iframe wildcard hostnames. ### Existing TLS proxy ```dotenv SELFHOST_TLS_MODE=external SELFHOST_TLS_EXTERNAL_BIND_ADDRESS=127.0.0.1 SELFHOST_TLS_EXTERNAL_PORT=8080 ``` Point your existing TLS terminator at this HTTP origin and preserve the original `Host` header. For a proxy on another host, bind to `0.0.0.0` only after restricting the VM firewall to that proxy. Create DNS records that point all three host patterns to the VM: ```text camel.example.com A/AAAA authenticate.example.com A/AAAA *.apps.example.com A/AAAA ``` ## Validate and start Generate the protected Pomerium configuration and check the host: ```bash bun run selfhost:configure bun run selfhost:doctor ``` `selfhost:doctor` checks the CLI versions, Docker daemon, CPU and memory, Docker socket access, domains, AI provider, authentication settings, Compose configuration, volumes, local service state, and bidirectional sandbox storage synchronization when the images are already local. Before first startup it may report that configured images are not local yet and defer the storage smoke. Start the stack: ```bash bun run selfhost:up ``` In source mode, this command builds all seven camelAI images before starting. ## Verify the installation Check readiness: ```bash curl --fail --silent http://127.0.0.1:3001/api/selfhost/health bun run selfhost:doctor ``` Then execute the storage check and all three attached runtimes: ```bash bun --env-file=.env.selfhost run selfhost:container:smoke:mount bun --env-file=.env.selfhost run selfhost:container:smoke:project bun --env-file=.env.selfhost run selfhost:container:smoke:analysis bun --env-file=.env.selfhost run selfhost:container:smoke:db-query ``` These are functional tests. They verify bidirectional local R2/container synchronization, create a real project-build container, run a real Jupyter notebook, and load the database-query drivers, then remove the smoke containers. > **Warning:** > > Do not grant sandbox containers `/dev/fuse`, `SYS_ADMIN`, or an unconfined > AppArmor profile. `selfhost-v0.1.4` and later use a FUSE-free synchronization > path for local R2 storage. Open `https://camel.example.com` and sign in through your identity provider. The wildcard app domain should not redirect to Pomerium because published public apps use a separate unauthenticated proxy route. The installation is ready when the health endpoint returns HTTP 200, `selfhost:doctor` passes, all four runtime smokes pass, and an OIDC user can open the camelAI control plane. Continue with [Operations](/docs/self-hosting/operations) to configure backups and learn the verified upgrade process. --- --- title: 'Authentication and Pomerium' description: 'Protect self-hosted camelAI with bundled Pomerium, an existing Pomerium deployment, or Cloudflare Access.' sidebarTitle: 'Authentication' --- A shared self-hosted installation must use an identity-aware proxy. Bundled Pomerium is the default and recommended option because it provides a complete OIDC sign-in path in the Docker Compose deployment. The camelAI control-plane hostname requires authentication. Deployed-app wildcard hosts route separately so an app can remain public when its owner chooses public visibility. ## Bundled Pomerium Bundled Pomerium runs in all-in-one mode with a persistent file-backed databroker. It validates users with your OIDC provider and sends a signed `X-Pomerium-Jwt-Assertion` to camelAI. ### Create the OIDC application Create a confidential web application in your identity provider with: | Setting | Value | | --------------------- | -------------------------------------------------- | | Grant type | Authorization Code | | Redirect URI | `https://authenticate.example.com/oauth2/callback` | | Scopes | `openid`, `email`, and `profile` | | Client authentication | Client secret | Use a distinct authenticate hostname. Do not use the camelAI control-plane hostname or a hostname inside the deployed-app wildcard. For a generic OIDC provider, set `POMERIUM_IDP_PROVIDER=oidc` and use the exact issuer URL as `POMERIUM_IDP_PROVIDER_URL`. The provider must publish an OIDC discovery document and JWKS. ### Configure Pomerium The minimum configuration is: ```dotenv SELFHOST_AUTH_MODE=bundled-pomerium SELFHOST_MAIN_HOSTNAME=camel.example.com POMERIUM_AUTHENTICATE_URL=https://authenticate.example.com POMERIUM_AUTHENTICATE_HOSTNAME=authenticate.example.com POMERIUM_ISSUER=camel.example.com POMERIUM_AUDIENCE=camel.example.com POMERIUM_DEFAULT_ORG_NAME=Example Corp POMERIUM_IDP_PROVIDER=oidc POMERIUM_IDP_PROVIDER_URL=https://idp.example.com/application/o/camelai/ POMERIUM_IDP_CLIENT_ID=camelai POMERIUM_IDP_CLIENT_SECRET=replace-with-your-client-secret ``` Run: ```bash bun run selfhost:configure ``` The command validates the hostnames and writes Pomerium's generated configuration and secret files under `.selfhost/pomerium/`. The client secret, cookie secret, and shared secret are mounted as protected files instead of being placed in the Pomerium container environment. On Linux, the Pomerium container retains only `DAC_OVERRIDE`, which lets it read the operator-owned `0600` bind mounts without making those files group-readable or world-readable. ### Configure TLS Caddy is the TLS front door for Docker Compose and AWS deployments. Pomerium always listens on plaintext `127.0.0.1:5444` behind Caddy and never binds a public port. Use automatic Cloudflare or Route 53 DNS validation for the simplest certificate setup. Operator-provided PEM files and an existing enterprise TLS terminator are also supported. See [Configure TLS and DNS](/docs/self-hosting/docker-compose#configure-tls-and-dns). ### Test the route After startup: 1. Open the camelAI hostname in a private browser window. 2. Confirm it redirects to the separate authenticate hostname. 3. Complete sign-in with a regular identity-provider user. 4. Confirm the browser returns to camelAI and creates or loads the configured organization. 5. Open an unused hostname under the app wildcard and confirm it does not redirect to Pomerium. The last request may return an app-side not-found response until an app is deployed. That confirms the public wildcard route reached camelAI without forcing a control-plane login. ## Existing Pomerium Use an operator-managed Pomerium deployment when your organization already has one: ```dotenv SELFHOST_AUTH_MODE=external-pomerium SELFHOST_TLS_MODE=external POMERIUM_AUTHENTICATE_URL=https://authenticate.example.com POMERIUM_JWKS_URL=https://camel.example.com/.well-known/pomerium/jwks.json POMERIUM_ISSUER=camel.example.com POMERIUM_AUDIENCE=camel.example.com POMERIUM_DEFAULT_ORG_NAME=Example Corp ``` Configure the external proxy to: * Require an authenticated user for the camelAI control-plane hostname * Preserve the original `Host` header * Pass Pomerium identity headers * Support WebSocket upgrades * Route public deployed-app wildcard hosts without forcing authentication ## Cloudflare Access Use an existing Cloudflare Access application with: ```dotenv SELFHOST_AUTH_MODE=cloudflare-access SELFHOST_TLS_MODE=external CLOUDFLARE_ACCESS_TEAM_DOMAIN=https://example.cloudflareaccess.com CLOUDFLARE_ACCESS_AUD=your-access-application-aud CLOUDFLARE_ACCESS_DEFAULT_ORG_NAME=Example Corp ``` The Access policy must protect the camelAI control-plane hostname and pass its signed identity assertion. Configure deployed-app wildcard routing separately according to the visibility policy you want for published apps. ## Local authentication bypass `LOCAL_AUTH_BYPASS=1` exists only for a loopback smoke test. Do not enable it on a shared VM or expand its allowed host list to a public hostname. ## Email-dependent accounts Password signup is rejected in self-host mode because camelAI cannot deliver the verification email. Provision users through Pomerium or Cloudflare Access. Organization invitations can still be created, but camelAI reports that email delivery is unavailable. Copy the invitation URL and send it through an approved internal channel. --- --- title: 'Deploy a single node on AWS' description: 'Provision self-hosted camelAI on EC2 with Terraform or CloudFormation.' sidebarTitle: 'AWS' --- camelAI includes Terraform and CloudFormation templates for the supported single-node deployment. Both templates implement the same Docker Compose release contract. They provision: * An Ubuntu 24.04 EC2 instance with IMDSv2 required * An encrypted gp3 data volume for the checkout, Docker data, and backups * A security group that exposes only the web proxy and optional break-glass SSH * SSM Session Manager access * Least-privilege Secrets Manager reads and Route 53 DNS-challenge access * An Elastic IP and optional Route53 records * Caddy in front of bundled Pomerium * A systemd service that pulls and starts the selected release * Installation-time project-build, notebook, and database-query smokes The default instance type is `t3a.xlarge`. The subnet and selected Availability Zone must match. ## Before you start You need: * AWS credentials that can create EC2, EBS, IAM, SSM, security-group, and optional Route53 resources * A VPC and subnet with outbound internet access, or permission to use the default VPC * A `selfhost-v*` release manifest with seven digest-pinned camelAI images and the tested Pomerium digest * A main hostname, authenticate hostname, and deployed-app wildcard domain * An OIDC application for bundled Pomerium * A Route 53 hosted zone for the default automatic-TLS path * Two AWS Secrets Manager secrets Store each secret as its raw `SecretString`, not as a JSON object: 1. The AI provider API key 2. The Pomerium OIDC client secret If your policy requires operator-provided certificates instead, create two additional secrets for the PEM certificate chain and matching unencrypted private key. > **Warning:** > > The AWS templates consume release images. Do not point a production stack at > mutable tags or combine images from different releases. ## Terraform Clone and check out the selected release: ```bash git clone https://github.com/qaml-ai/camelAI.git cd camelAI git checkout --detach selfhost-vX.Y.Z cd infra/selfhost/terraform cp terraform.tfvars.example terraform.tfvars ``` Edit `terraform.tfvars`. At minimum, replace: * `repository_ref` * All seven camelAI `*_image` values and `pomerium_image` * The main, authenticate, and wildcard hostnames * Route53 settings, or disable automatic DNS creation * The AI and Pomerium secret ARNs * `tls_mode = "automatic"` and the Route 53 hosted-zone ID * The OIDC issuer and client ID Then run: ```bash terraform init terraform plan terraform apply ``` Terraform waits for the VM bootstrap marker through an SSM association. The apply fails if Compose startup, the health check, or any attached-runtime smoke fails. Use the outputs to inspect the host: ```bash aws ssm start-session --target sudo cloud-init status --wait sudo journalctl -u camelai-selfhost --no-pager sudo camelai-selfhost-compose logs --tail=200 caddy pomerium app ``` Prefer SSM Session Manager. Leave `ssh_ingress_cidrs` empty unless you need narrowly scoped break-glass SSH access. ## CloudFormation Deploy the template into a subnet with outbound internet access: ```bash aws cloudformation deploy \ --stack-name camelai-selfhost \ --template-file infra/selfhost/cloudformation/aws-single-node.yaml \ --capabilities CAPABILITY_IAM \ --parameter-overrides \ VpcId=vpc-... \ SubnetId=subnet-... \ AvailabilityZone=us-east-1a \ RepositoryRef=selfhost-vX.Y.Z \ AppImage=ghcr.io/qaml-ai/camelai-selfhost-app@sha256:... \ LocalArtifactsImage=ghcr.io/qaml-ai/camelai-selfhost-local-artifacts@sha256:... \ ProjectBuildImage=ghcr.io/qaml-ai/camelai-selfhost-project-build@sha256:... \ AnalysisImage=ghcr.io/qaml-ai/camelai-selfhost-analysis@sha256:... \ DbQueryImage=ghcr.io/qaml-ai/camelai-selfhost-db-query@sha256:... \ ContainerEgressImage=ghcr.io/qaml-ai/camelai-selfhost-container-egress@sha256:... \ CaddyImage=ghcr.io/qaml-ai/camelai-selfhost-caddy@sha256:... \ PomeriumImage=pomerium/pomerium@sha256:... \ MainHostname=camel.example.com \ AppVanityDomain=apps.example.com \ AuthProvider=bundled-pomerium \ AuthDefaultOrgName='Example Corp' \ PomeriumAuthenticateUrl=https://authenticate.example.com \ PomeriumAuthenticateHostname=authenticate.example.com \ PomeriumIdpProvider=oidc \ PomeriumIdpProviderUrl=https://idp.example.com/application/o/camelai/ \ PomeriumIdpClientId=camelai \ PomeriumIdpClientSecretArn=arn:aws:secretsmanager:... \ SelfhostAiApiKeySecretArn=arn:aws:secretsmanager:... \ TlsMode=automatic \ Route53HostedZoneId=Z0123456789EXAMPLE ``` CloudFormation waits up to 30 minutes for bootstrap. Its wait condition receives success only after the HTTP health check and all three runtime smokes pass. ## TLS and network modes The default `automatic` mode gives the EC2 role narrowly scoped permission to create and remove TXT ACME challenge records in `Route53HostedZoneId`. Caddy obtains and renews the main, authenticate, and wildcard certificates automatically. `CreateRoute53Records` separately controls whether the stack creates the public A records. Use `provided` mode when automated DNS credentials are not permitted. It reads the certificate and key from Secrets Manager; both secret ARN parameters are then required. The certificate must cover the main, authenticate, deployed-app wildcard, and any separate iframe wildcard hostnames. Use external TLS only behind a load balancer or identity proxy. In external mode, port 80 is an origin port. Restrict `web_ingress_cidrs` or `WebIngressCidr` to the upstream proxy and never expose it directly to the internet. When an external load balancer terminates TLS, the VM must be able to reach its public camelAI HTTPS hostname through that load balancer. camelAI uses that path to retrieve Pomerium's signing keys. ## Deletion and recovery The encrypted data volume has snapshot retention configured when the CloudFormation stack is deleted. Before deleting a production stack: 1. Run and verify an application backup. 2. Confirm the data-volume snapshot policy and resulting snapshot. 3. Retain the AI and OIDC secrets, plus TLS secrets when using provided mode. 4. Record the release manifest and exact image digests. See [Operations](/docs/self-hosting/operations) for application backups, restores, and verified upgrades. --- --- title: 'Customize camelCode' description: 'Teach a self-hosted camelAI agent about internal APIs, application standards, and organization-specific workflows.' sidebarTitle: 'Agent customization' --- Self-hosted camelAI can load organization-specific instructions and skills without rebuilding its application image. Use this to teach camelCode how to build web apps and sites that use your internal systems. > **Info:** > > File-based agent customization requires `selfhost-v0.1.2` or later. Agent customization is additive. Your instructions do not replace camelAI's stock safety, tool-use, and verification rules. ## Choose between a prompt and a skill Use `.selfhost/agent/prompt.append.md` for short rules that should apply to every task, such as an approved package registry or a requirement to include an audit log. Use a custom skill for detailed instructions that apply only to certain work. Skills are a better place for internal API contracts, application templates, authentication patterns, design-system rules, and deployment checklists. The agent sees each skill's name and description, then reads its full instructions when a task matches. ## Agent pack layout `bun run selfhost:init` creates `.selfhost/agent/` and its `skills/` directory: ```text .selfhost/agent/ README.md prompt.append.md prompt.prepend.md skills/ internal-web-apps/ SKILL.md references/ internal-api.md design-system.md ``` * `prompt.append.md` adds deployment instructions after the stock system prompt. * `prompt.prepend.md` adds optional policy or identity text before the stock prompt. * Each directory under `skills/` is one skill and must contain `SKILL.md`. * A skill can include additional Markdown files under any nested directory. ## Add instructions for every task Create `.selfhost/agent/prompt.append.md` with rules that should always apply: ```markdown title=".selfhost/agent/prompt.append.md" Use the approved internal package registry when adding dependencies. When a user asks for an internal web app or site, read the `internal-web-apps` skill before writing code. Never place API keys, access tokens, or customer data in source files. ``` Keep this file short. Put detailed contracts and examples in a skill so the agent only loads them when they are relevant. ## Add an internal application skill Create `.selfhost/agent/skills/internal-web-apps/SKILL.md`: ```markdown title=".selfhost/agent/skills/internal-web-apps/SKILL.md" --- name: internal-web-apps description: Build web apps and sites that use ExampleCorp internal systems. Use for internal portals, dashboards, and workflow tools. --- # Internal web apps Before writing code: 1. Read `references/internal-api.md` with `read_skill({ skill: "internal-web-apps", file: "references/internal-api.md" })`. 2. Read `references/design-system.md` before creating the interface. 3. Use only documented endpoints, fields, and authentication methods. 4. Confirm that the deployed app can reach the required internal service. 5. Never copy credentials into source code, logs, or generated documentation. When the requested integration is not documented, stop and ask for the missing contract instead of inventing an endpoint. ``` Then add the referenced API contract: ```markdown title=".selfhost/agent/skills/internal-web-apps/references/internal-api.md" # Internal API contract Base URL: `https://internal-api.example.com` ## Authentication Describe how the deployed app receives authorization without including the credential itself. Name the exact header, Worker binding, or approved broker that is already configured for the app. ## Endpoints Document each approved method, path, request schema, response schema, and error shape. Include a minimal working request and response for each common workflow. ## Network requirements Document DNS, firewall, private-network, or proxy requirements that must be in place before a deployed app can reach this service. ``` Replace the example values with your verified internal contract. Keep secrets out of the skill files. Skill content gives the agent instructions; it does not create network access, credentials, service bindings, or permissions for the deployed application. ## Apply the agent pack After adding or changing prompt and skill files, regenerate the self-host configuration and restart the application: ```bash bun run selfhost:configure bun run selfhost:up ``` Confirm what loaded: ```bash bun run selfhost:doctor curl --fail --silent http://127.0.0.1:3001/api/selfhost/health ``` The doctor lists custom skill names. The health response includes an `agent-pack` check that summarizes the loaded prompt and skills. Start a new camelAI task and ask it to build an internal app to verify that it selects and reads the skill. ## Connection sharing and internal APIs Custom skills do not change the deployed-app connection boundary. If you set: ```dotenv CONNECTIONS_BINDING_ENABLED=false ``` camelCode can still use workspace connections while it works, but deployed apps cannot call `env.CONNECTIONS` or the connection catalogs behind their `ANALYSIS` and `WAREHOUSE` bindings. In that mode, teach the agent to use only an internal API or app-specific integration that you have separately made reachable and authorized. See [Install with Docker Compose](/docs/self-hosting/docker-compose#choose-whether-deployed-apps-can-use-connections) for the full setting behavior and rollout steps. ## Advanced path overrides Most installations should use `.selfhost/agent/`. These environment variables are available when the agent pack must live elsewhere or be supplied by an automation system: | Variable | Purpose | | ------------------------------- | -------------------------------------------------------- | | `SELFHOST_AGENT_HOST_DIR` | Host directory mounted read-only into the app container | | `SELFHOST_AGENT_DIR` | Agent-pack directory read by the configuration generator | | `SELFHOST_AGENT_SKILLS_DIR` | Alternate directory containing skill subdirectories | | `SELFHOST_AGENT_PROMPT_APPEND` | Inline override for `prompt.append.md` | | `SELFHOST_AGENT_PROMPT_PREPEND` | Inline override for `prompt.prepend.md` | | `SELFHOST_AGENT_SKILLS_JSON` | Serialized skill payload for automated deployments | Non-empty environment overrides take precedence over the corresponding files. A custom skill with the same name as a bundled camelAI skill overrides that bundled skill, so use unique names unless replacement is intentional. --- --- title: 'Operate a self-hosted installation' description: 'Monitor, validate, back up, restore, and upgrade a self-hosted camelAI node.' sidebarTitle: 'Operations' --- Run operational commands from the checked-out camelAI repository on the VM. Keep `.env.selfhost` readable only by the operator account because it contains identity, signing, integration, and AI-provider secrets. ## Check health Use both the readiness endpoint and the host doctor: ```bash curl --fail --silent http://127.0.0.1:3001/api/selfhost/health bun run selfhost:doctor ``` The health response includes a versioned capability contract. Builds, deploys, notebooks, and SQL should report `state: "configured"`. `configured` means the Docker image and `workerd` namespace are wired. It does not mean a functional job ran during the lightweight HTTP check. The response therefore includes a verification command for each attached runtime. Missing required runtime bindings make readiness fail with HTTP 503. Intentionally disabled outbound email and SMTP do not make the service unhealthy. ## Run attached-runtime smokes Run all four smokes after initial installation, after an upgrade, and after a Docker or host-networking change: ```bash bun --env-file=.env.selfhost run selfhost:container:smoke:mount bun --env-file=.env.selfhost run selfhost:container:smoke:project bun --env-file=.env.selfhost run selfhost:container:smoke:analysis bun --env-file=.env.selfhost run selfhost:container:smoke:db-query ``` The mount smoke verifies bidirectional synchronization between local R2 storage and a sandbox container. The project smoke launches a build container and executes a small build. The analysis smoke starts Jupyter and executes a notebook. The database-query smoke loads and exercises the supported query drivers. Each command removes its smoke container afterward. The mount smoke does not require `/dev/fuse`, `SYS_ADMIN`, or an unconfined AppArmor profile. A supported self-host installation must not grant those privileges to containers that execute generated code. ## Inspect containers and logs For the default bundled-Pomerium automatic-TLS deployment: ```bash docker compose \ --env-file .env.selfhost \ -f docker-compose.selfhost.yml \ -f docker-compose.selfhost.caddy.yml \ -f docker-compose.selfhost.pomerium.yml \ -f docker-compose.selfhost.pomerium-loopback.yml \ ps docker compose \ --env-file .env.selfhost \ -f docker-compose.selfhost.yml \ -f docker-compose.selfhost.caddy.yml \ -f docker-compose.selfhost.pomerium.yml \ -f docker-compose.selfhost.pomerium-loopback.yml \ logs --tail=200 caddy pomerium app ``` If you use external Pomerium or Cloudflare Access, omit both Pomerium overlay files but keep the Caddy overlay as the restricted HTTP origin. On an AWS-provisioned host, also inspect: ```bash sudo cloud-init status --wait sudo journalctl -u camelai-selfhost --no-pager sudo camelai-selfhost-compose logs --tail=200 caddy pomerium app ``` ## Back up Create a backup of durable application state, local Artifact repositories, Pomerium's databroker, and Caddy's certificate/account state: ```bash bun run selfhost:backup ``` Backups are stored under `.selfhost/backups/`. `.env.selfhost` is intentionally excluded because it contains secrets. Store a separate encrypted copy with access controls appropriate for your organization. Also retain any provided TLS material and the release manifest used by the installation. Copy backups off the VM and test restores on a separate node on a regular schedule. A backup that exists only on the application disk does not protect against disk or instance loss. ## Restore Stop the stack before restoring: ```bash docker compose \ --env-file .env.selfhost \ -f docker-compose.selfhost.yml \ -f docker-compose.selfhost.caddy.yml \ -f docker-compose.selfhost.pomerium.yml \ -f docker-compose.selfhost.pomerium-loopback.yml \ down bun run selfhost:restore -- .selfhost/backups/ ``` Restore the matching `.env.selfhost`, any provided TLS material, code release, and image digests before starting. Then run the doctor, start the stack, and execute all four runtime smokes. ## Upgrade Download `selfhost-release.json` and `selfhost-upgrade-bootstrap.mjs` from the matching [camelAI GitHub Release](https://github.com/qaml-ai/camelAI/releases). Keep both files outside the mutable repository checkout. For example: ```bash RELEASE=selfhost-vX.Y.Z curl --fail --location \ --output /secure/path/selfhost-release.json \ "https://github.com/qaml-ai/camelAI/releases/download/${RELEASE}/selfhost-release.json" curl --fail --location \ --output /secure/path/selfhost-upgrade-bootstrap.mjs \ "https://github.com/qaml-ai/camelAI/releases/download/${RELEASE}/selfhost-upgrade-bootstrap.mjs" ``` Run: ```bash bun run selfhost:upgrade -- \ --release selfhost-vX.Y.Z \ --manifest /secure/path/selfhost-release.json ``` The upgrader: 1. Requires a clean tracked worktree. 2. Verifies the target Git revision and all eight digest-pinned images. 3. Backs up durable volumes. 4. Saves the previous checkout and `.env.selfhost`. 5. Hands control to the target release's upgrader. 6. Pulls and starts the target images. 7. Runs the doctor and all four attached-runtime smokes. 8. Prints the exact rollback command. For an installation whose current upgrader predates target-code handoff, run the standalone bootstrap once: ```bash node /secure/path/selfhost-upgrade-bootstrap.mjs \ --repo "$PWD" \ --release selfhost-vX.Y.Z \ --manifest /secure/path/selfhost-release.json ``` Use the exact rollback path printed by a successful upgrade: ```bash bun run selfhost:upgrade -- --rollback .selfhost/releases/ ``` > **Warning:** > > Runtime rollback does not reverse D1 migrations. If a schema rollback is > required, stop the stack and restore the matching pre-upgrade volume backup > before starting the previous release. AWS-provisioned hosts install a wrapper around the same verified upgrader: ```bash sudo camelai-selfhost-upgrade \ --release selfhost-vX.Y.Z \ --manifest /secure/path/selfhost-release.json ``` ## Protect the Docker host The application must have read-write access to `/var/run/docker.sock` to create build, notebook, query, and egress containers. Treat application-container control as root-equivalent VM access. * Do not expose the Docker API over TCP. * Keep the camelAI application and local Artifact ports on loopback. * Restrict host administration to SSM or a narrow management network. * Apply operating-system and Docker security updates on a tested schedule. * Monitor disk use under Docker's data root and the backup directory. * Restrict outbound traffic according to your provider and package-registry needs. * Protect `.env.selfhost`, Pomerium and Caddy secrets, TLS keys, and backups. The app service uses Linux host networking because the attached container egress sidecars communicate through loopback. Changing it to a nested Compose bridge prevents the runtime containers from becoming ready. ## Outbound email Outbound email is disabled. Setting `EMAIL_FROM_ADDRESS`, `WORKSPACE_EMAIL_DOMAIN`, or SMTP-looking environment variables does not enable delivery. Until a supported internal SMTP transport is implemented: * Provision users through Pomerium or Cloudflare Access. * Deliver invitation URLs through an approved internal channel. * Route support requests through your existing support process. * Do not add a fake SMTP sink that accepts and discards messages. ## Troubleshooting ### Pomerium cannot read its configuration Keep `.selfhost/pomerium/config.yaml` and secret files owned by the operator and mode `0600`. Use the repository's Pomerium Compose overlay, which retains the minimal `DAC_OVERRIDE` capability needed to read those protected bind mounts on Linux. ### A runtime container never becomes ready Confirm: * The host is x86\_64. * Docker has at least 4 CPUs and 7.5 GiB usable memory. * The application container uses host networking. * The Docker socket is mounted read-write. * The configured runtime and container-egress images come from one release. * Host firewall rules do not block Docker bridge control traffic. Do not substitute Cloudflare's stock `cloudflare/proxy-everything` sidecar for the manifest's `SELFHOST_CONTAINER_EGRESS_IMAGE`. The release image contains a bridge-bypass entrypoint for hosts where the stock sidecar intercepts Docker control traffic. If the VM runs a host firewall, inspect its Docker bridge policy and add the narrowest rule that permits container-to-host control traffic. For UFW, the equivalent broad interface rule is `sudo ufw allow in on docker0`; review it against your network policy before applying it. A host without a blocking firewall does not need that rule. Run `bun run selfhost:doctor`, then run the failing runtime's smoke command. ### App deployment reports an S3FS or `/dev/fuse` mount failure Upgrade to `selfhost-v0.1.4` or later. Earlier releases could ask local sandbox containers to use Cloudflare's production S3FS mount path, which is not available to stock self-host containers. Do not add `/dev/fuse`, `SYS_ADMIN`, or `apparmor=unconfined` to the sandbox containers. Current releases use the Sandbox SDK's FUSE-free local R2 synchronization path instead. Verify it directly: ```bash bun --env-file=.env.selfhost run selfhost:container:smoke:mount bun run selfhost:doctor ``` This is a persistent version/configuration error on an affected release, not a transient outage. Repeated deployment retries will not fix it. ### Caddy cannot obtain or renew a certificate Confirm that: * The selected DNS credential can read the zone and edit TXT records. * `SELFHOST_TLS_DNS_PROVIDER` matches where the authoritative zone is hosted. * The Route 53 hosted-zone ID is correct, or the Cloudflare token is scoped to the correct zone. * Outbound HTTPS and DNS resolution work from the VM. Run `bun run selfhost:configure`, inspect `docker compose ... logs caddy`, and retry only after correcting the credential or zone. Do not work around DNS validation by exposing Pomerium's plaintext upstream. ### The control plane returns a redirect loop Confirm that: * `SELFHOST_PUBLIC_BASE_URL` and `SELFHOST_MAIN_HOSTNAME` describe the same host. * `POMERIUM_AUTHENTICATE_URL` uses a distinct host. * The original `Host` header reaches Pomerium and camelAI. * The certificate covers both hosts. * The VM can resolve the public hostnames. ### Public apps redirect to sign-in The control-plane route and deployed-app wildcard route must remain separate. Regenerate bundled Pomerium configuration with: ```bash bun run selfhost:configure ``` For an external proxy, configure the wildcard app route without forced authentication and let camelAI enforce each app's public or private setting. --- --- title: 'Track per-user LLM usage' description: 'Report LLM spend by user, provider, and model on a self-hosted camelAI installation.' sidebarTitle: 'Usage tracking' --- Self-hosted camelAI records every covered LLM pull with its user, provider, model, and token counts, plus the calculated cost when pricing is known. Read the records through the local admin API for per-user spend reports, filtered totals, or a detailed log. [Spend limits](/docs/self-hosting/spend-limits) enforce rolling caps against the same records. > **Info:** > > Usage tracking requires **selfhost-v0.1.14** or later and has no product UI. > If you are on an earlier release, [upgrade your > installation](/docs/self-hosting/operations#upgrade) first. ## Admin API access Send requests to the internal application URL and authenticate with the `ADMIN_API_KEY` value from `.env.selfhost`: ```bash curl --fail --silent \ --header "Authorization: Bearer ${ADMIN_API_KEY}" \ "http://127.0.0.1:3001/api/admin/orgs?limit=100" ``` The internal URL is `SELFHOST_INTERNAL_APP_URL`, or `http://127.0.0.1:3001` when unset. It bypasses the interactive Pomerium login, so run requests on the host itself. Installations created on selfhost-v0.1.14 or later already have the key; an older `.env.selfhost` gets one from `bun run selfhost:migrate-secrets` or a normal `bun run selfhost:up`. > **Warning:** > > Keep the admin API key on the host. camelAI never passes it to project > containers, sandboxes, or deployed apps. Do not commit it or send it in > email or chat. ## What is recorded Each pull records the user, organization, provider, model, input and output token counts, cache creation and cache read token counts, and the cost in USD when pricing is known. Tracking covers main agents, child agents, context compaction, and attributable virtual-AI calls. Auxiliary title generation, completion summaries, and chat-group icon generation are not recorded in this version, so reported usage can be slightly lower than your provider's bill. ## Report usage `GET /api/admin/orgs` and `GET /api/admin/users` list organization and user IDs. Report ranges are Unix-millisecond timestamps; `from` is inclusive and `to` is exclusive. * `GET /api/admin/orgs/{ORG_ID}/usage/users?from={FROM_MS}&to={TO_MS}` returns spend per user, grouped by provider and model. * `GET /api/admin/orgs/{ORG_ID}/usage/log` returns individual usage records. * `GET /api/admin/orgs/{ORG_ID}/usage/log/sum` returns cost, request, and token totals. The log endpoints accept the same range and filter by `user_id`, `provider`, `model`, `usage_kind`, and `usage_surface`. List responses paginate with `limit` and `cursor`. For example, the last 30 days of per-user spend: ```bash NOW_MS=$(date +%s000) FROM_MS=$((NOW_MS - 30 * 24 * 60 * 60 * 1000)) curl --fail --silent \ --header "Authorization: Bearer ${ADMIN_API_KEY}" \ "http://127.0.0.1:3001/api/admin/orgs/${ORG_ID}/usage/users?from=${FROM_MS}&to=${NOW_MS}" ``` ## Model pricing Costs come from the built-in pricing catalog, which covers the stock provider models. A custom model ID without catalog pricing records tokens but no cost until you add an exact override for its provider and model. `GET /api/admin/orgs/{ORG_ID}/usage/pricing` returns the current overrides and recently seen unpriced models. `PUT` to the same path replaces the complete override list: ```json { "prices": [ { "provider": "custom", "model": "acme-code-70b", "input_usd_per_million": 0.8, "output_usd_per_million": 2.4, "cache_creation_usd_per_million": 1, "cache_read_usd_per_million": 0.08 } ] } ``` The `PUT` is a full replacement, not an upsert: read the current list, merge your change, and send the complete result, or you will delete any override you leave out. There are no default rates for unknown models, so take the exact prices from your provider's price list. An all-zero override is valid only for a genuinely free model. Overrides apply to usage recorded after the change; existing rows are never repriced. Unpriced usage appears in reports without a cost, and it blocks any user who has an active [spend limit](/docs/self-hosting/spend-limits#unknown-pricing-fails-closed). --- --- title: 'Set per-user spend limits' description: 'Enforce rolling LLM spend caps for each user on a self-hosted camelAI installation.' sidebarTitle: 'Spend limits' --- Self-hosted camelAI can cap each user's rolling LLM spend. When a user's settled spend reaches a limit, camelAI denies new LLM pulls for that user until enough usage ages out of the window. Limits are enforced against [tracked usage](/docs/self-hosting/usage-tracking) and managed through the same [admin API](/docs/self-hosting/usage-tracking#admin-api-access). > **Info:** > > Spend limits require **selfhost-v0.1.14** or later and have no product UI. ## Set limits A user's limits live at `/api/admin/orgs/{ORG_ID}/usage/users/{USER_ID}/limits`. `GET` returns the current limits and each window's spend. `PUT` replaces the full list: ```bash curl --fail --silent --request PUT \ --header "Authorization: Bearer ${ADMIN_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "limits": [ { "window_hours": 24, "limit_usd": 5, "label": "daily" }, { "window_hours": 720, "limit_usd": 50, "label": "30-day" } ] }' \ "http://127.0.0.1:3001/api/admin/orgs/${ORG_ID}/usage/users/${USER_ID}/limits" ``` Each limit caps USD spend over the preceding `window_hours`. A user can have several limits at once, and all of them apply. Before limiting a user who uses custom model IDs, add [pricing overrides](/docs/self-hosting/usage-tracking#model-pricing) for those models. Unpriced usage blocks a limited user entirely. ## How enforcement works camelAI checks limits before accepting each pull, not during one. A pull that was already accepted runs to completion even if it crosses the cap, and concurrent pulls can settle after crossing it, so a user can slightly exceed a limit. Later pulls are denied until enough usage ages out of the window. ## Unknown pricing fails closed For a user with an active limit, camelAI denies a pull when it cannot price the requested model, or when any unpriced usage sits inside an active window. Built-in models are priced automatically, so this mainly affects custom model IDs. A [pricing override](/docs/self-hosting/usage-tracking#model-pricing) makes future pulls for that provider and model eligible, but existing unpriced rows are never repriced. They keep blocking until they age out of every active window or you clear the user's limits. ## Clear limits `PUT` an empty list to the same endpoint to remove a user's limits: ```bash curl --fail --silent --request PUT \ --header "Authorization: Bearer ${ADMIN_API_KEY}" \ --header "Content-Type: application/json" \ --data '{ "limits": [] }' \ "http://127.0.0.1:3001/api/admin/orgs/${ORG_ID}/usage/users/${USER_ID}/limits" ``` Clearing limits also unblocks a user whose active window contains unpriced usage. --- --- title: 'camelRun: the hosted, open-source AI agent runtime' sidebarTitle: 'Overview' description: 'camelRun hosts durable AI agents: your tools stay in your code, and it runs the model loop, keeps history and files, and wakes agents when work arrives.' --- [View as Markdown](https://camelai.com/docs/camelrun/overview.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) > **Tip:** > > **Set up with your coding agent.** Paste this into Claude Code, Codex, Cursor > or any coding agent. It asks you what the agent should do, installs the SDK, > connects your account without pasting keys into chat, and runs your first > agent. > > ```text > Read https://run.camelai.com/SKILL.md and set up camelRun in this project. Ask me what the agent should do. > ``` > **Self-host it.** The runtime is open source and runs anywhere with Docker: one > container and Postgres. See [Self-hosting](/docs/camelrun/self-hosting). camelRun is camelAI's hosted agent runtime. You define an agent's tools in your own code with the TypeScript or Python SDK, or serve them over HTTP. camelRun runs the model loop, keeps each agent's history and files, compacts long conversations, runs model-written code in a sandbox, and wakes agents when there is work. Most agents are asleep at any moment and cost only storage while they are. For what camelRun is for and what it costs, see [AI agent hosting with camelRun](https://camelai.com/run). To compare it with other managed agent platforms, read [managed agents compared](https://camelai.com/run/vs). ```ts TypeScript import { Agents } from "@camelai/run"; const agents = new Agents(); // reads CAMELAI_API_KEY const agent = await agents.upsert("support-triage", { instructions: "Be concise.", }); console.log((await agent.run("Which tickets look urgent?")).text); await agents.close(); ``` ```python Python import asyncio from camelai_run import Agents async def main(): async with Agents() as agents: # reads CAMELAI_API_KEY agent = await agents.upsert("support-triage", instructions="Be concise.") print((await agent.run("Which tickets look urgent?")).text) asyncio.run(main()) ``` | | | | ------------------- | ------------------------------------------------------------- | | **API** | `https://run.camelai.com/v1` | | **Console** | [run.camelai.com/console](https://run.camelai.com/console) | | **OpenAPI spec** | [`/v1/openapi.json`](https://run.camelai.com/v1/openapi.json) | | **Docs for agents** | [`/llms.txt`](https://run.camelai.com/llms.txt) | | **Source** | [github.com/qaml-ai/run](https://github.com/qaml-ai/run) | ## Connect your coding agent The hosted MCP server at `https://run.camelai.com/mcp` needs no API key: sign in with GitHub or Google when your client asks, which creates your account. GitHub accounts older than 30 days start with free credit; when the [Billing](https://run.camelai.com/console/billing) page offers it, everyone else can unlock it by verifying a card (no charge). ### Claude Code ```bash claude mcp add --transport http camelrun https://run.camelai.com/mcp # Then run /mcp in Claude Code to sign in. Or skip signing in with a key from your environment: claude mcp add --scope project --transport http camelrun https://run.camelai.com/mcp \ --header 'Authorization: Bearer ${CAMELAI_API_KEY}' ``` The single quotes keep `${CAMELAI_API_KEY}` as written: the project's `.mcp.json` names the variable, and Claude Code reads its value from the environment. Never write a key's value into an MCP config; they are often committed. ### Codex ```bash codex mcp add camelrun --url https://run.camelai.com/mcp codex mcp login camelrun ``` ### Cursor [Add camelRun to Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=camelrun\&config=eyJ1cmwiOiJodHRwczovL3J1bi5jYW1lbGFpLmNvbS9tY3AifQ==), or add it to `.cursor/mcp.json`: ```json { "mcpServers": { "camelrun": { "url": "https://run.camelai.com/mcp" } } } ``` ### VS Code [Add camelRun to VS Code](https://vscode.dev/redirect/mcp/install?name=camelrun\&config=%7B%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Frun.camelai.com%2Fmcp%22%7D), then start the server and sign in when VS Code asks. ### Claude In claude.ai or the desktop app, go to **Settings**, then **Connectors**, then **Add custom connector**, and enter `https://run.camelai.com/mcp`. ## What you get ### Durable, keyed agents The same key is the same agent, with its history and files, from any process, until you delete it. ### Tools in your code Tools run in your process or on your HTTPS endpoint, with your credentials. MCP servers and OpenAPI specs work too. ### People in the loop Approvals, questions and forms suspend a run for days, then resume it where it stopped. ### Chat UI in minutes React components, and bindings for Vue, Svelte and Solid, stream an agent into your app without exposing your API key. ### Channels Slack, Telegram, Discord, GitHub and signed webhooks from other services can start and talk to agents. ### [Agents as MCP servers](/docs/camelrun/mcp-server) Every agent is an MCP server, so Claude, Cursor or any MCP client can message it. ### [Any model](/docs/camelrun/models-and-keys) Pick from the catalog, bring your own provider keys, or add a model server of your own. ### [Scheduled agents and email reports](/docs/camelrun/schedules) Wake an agent daily or on weekdays at a local time, and email its report. ### [Stateless runs](/docs/camelrun/stateless-runs) One call for one answer: classify, extract or judge, with no agent kept. ### [Workspaces](/docs/camelrun/workspaces) Group agents and definitions by project, and see model and tool spend for each. ### [Computers](/docs/camelrun/computers) Give an agent a Linux computer or a remote browser from Modal, Daytona, E2B, Browserbase and more, or your own through Cloudflare Tunnel. ### [Projects](/docs/camelrun/projects) An agent builds a bot, site or report; your app checks and publishes its versions. ### [Voice and images](/docs/camelrun/voice) Audio in messages is transcribed for the model, agents talk in Discord voice calls, and they make and edit images. ### [End-user credits](/docs/camelrun/end-user-credits) Charge your own users for what their agents spend, with balances and daily allowances per person. ## Start here ### [Quickstart](/docs/camelrun/quickstart) An agent that calls a tool in your process, in five minutes. ### [Build a chat app](/docs/camelrun/chat-app) A streaming chat with your users' agents, from one command. ### [Concepts](/docs/camelrun/concepts) Keyed agents, runs, events, and where tools run. ### [CLI and MCP server](/docs/camelrun/cli) Deploy and manage agents from a terminal, or from a coding agent. ## Packages | Package | | | ------------------------- | -------------------------------------------------------------------------------------------------------- | | `@camelai/run` (npm) | TypeScript SDK: Node 22+ (on Node 26, 26.5.1 or later), Bun, Deno, Cloudflare Workers | | `camelai-run` (PyPI) | Python SDK: Python 3.11+ | | `@camelai/run-react` | Chat components and hooks. Vue, Svelte and Solid bindings are `@camelai/run-vue`, `-svelte` and `-solid` | | `@camelai/create-run-app` | `npm create @camelai/run-app` scaffolds a Next.js chat app | | `@camelai/camelrun` | The `camelrun` CLI and local MCP server | ## Licensing The camelRun runtime is licensed under the GNU AGPL-3.0. The SDKs, frontend packages, CLI and examples are MIT, so you can use them in any application. Commercial licenses for the runtime are available: contact us through [camelai.com](https://camelai.com). See [Self-hosting](/docs/camelrun/self-hosting) to run the runtime yourself. --- --- title: 'camelRun quickstart: run your first AI agent' sidebarTitle: 'Quickstart' description: 'Run your first AI agent on camelRun with the TypeScript or Python SDK: it calls a tool in your own process and answers you.' --- [View as Markdown](https://camelai.com/docs/camelrun/quickstart.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) > **Tip:** > > **Set up with your coding agent.** Paste this into Claude Code, Codex, Cursor > or any coding agent. It asks you what the agent should do, installs the SDK, > connects your account without pasting keys into chat, and runs your first > agent. > > ```text > Read https://run.camelai.com/SKILL.md and set up camelRun in this project. Ask me what the agent should do. > ``` In five minutes you will have an agent that runs in camelRun, calls a tool in your own process, and answers you. camelRun keeps the model loop, the agent's history and its files. Your tools stay in your code, with your credentials. ## Credit Signing in to the [console](https://run.camelai.com/console) with GitHub or Google creates your account. GitHub accounts older than 30 days start with free credit. When the [Billing](https://run.camelai.com/console/billing) page offers it, everyone else (Google sign-ins, newer GitHub accounts) unlocks the same starting credit by verifying a card: the card is saved, not charged. Runs use credit even when you bring your own provider key. ## Try it from your coding agent (no key) Connect the hosted MCP server and sign in. There is no key to create or copy. ### Add the camelRun MCP server ```bash Claude Code claude mcp add --transport http camelrun https://run.camelai.com/mcp # then run /mcp in Claude Code and sign in with GitHub or Google ``` ```bash Codex codex mcp add camelrun --url https://run.camelai.com/mcp codex mcp login camelrun ``` Cursor, VS Code and Claude: see [Connect your coding agent](/docs/camelrun/cli#connect-your-coding-agent). ### Ask for an agent ```text Use the camelrun MCP server to create my first camelRun agent: ask me what it should do, create it, run it once, and show me its reply. ``` Your agent appears under **Agents** in the [console](https://run.camelai.com/console). ## Build it into your app ### Get an API key Sign in to the [console](https://run.camelai.com/console). Under [**API keys**](https://run.camelai.com/console/tokens), create a key and export it: ```bash export CAMELAI_API_KEY=art_... ``` The key can create and control every agent in your account. Keep it on your server and never ship it to a browser. ### Install the SDK ```bash TypeScript # Node 22 or later (on Node 26: 26.5.1 or later), or Bun npm install @camelai/run ``` ```bash Python # Python 3.11 or later pip install camelai-run ``` ### Run an agent ```ts quickstart.mts import { Agents, schema, tool } from "@camelai/run"; const agents = new Agents(); // reads CAMELAI_API_KEY // A tool is an ordinary function. It runs here, in your process. const weather = tool({ description: "Today's weather in a city", input: schema.Object({ city: schema.String() }), execute: ({ city }) => ({ city, forecast: "sunny", highC: 24 }), }); // The same key is the same agent, with its history, every time you run this. const agent = await agents.upsert("quickstart", { instructions: "You are a concise assistant.", tools: { weather }, }); const run = await agent.run("Should I bring an umbrella in Lisbon today?"); console.log(run.text); await agents.close(); ``` ```python quickstart.py import asyncio from camelai_run import Agents, tool @tool def weather(city: str) -> dict: """Today's weather in a city""" return {"city": city, "forecast": "sunny", "highC": 24} async def main(): async with Agents() as agents: # reads CAMELAI_API_KEY agent = await agents.upsert( "quickstart", instructions="You are a concise assistant.", tools=[weather], ) run = await agent.run("Should I bring an umbrella in Lisbon today?") print(run.text) asyncio.run(main()) ``` Run it with `npx tsx quickstart.mts` or `python quickstart.py`. `.mts` makes the TypeScript an ES module, which top-level `await` needs; in a project with `"type": "module"`, `.ts` works too. The Python SDK is async; synchronous code (a script, a Django view, a Celery task) uses `camelai_run.sync`, which has the same names without `await`. It doesn't serve tools from your process, as this example does; see [SDKs](/docs/camelrun/sdks#the-synchronous-python-client). The examples name no model, so the agent gets your account's default: the first of the platform's default models your account can use. ### Choose a model (optional) List the models your account can use, then name one: ```bash CLI npx -y @camelai/camelrun models --available ``` ```bash curl curl -s "https://run.camelai.com/v1/models?available=true" \ -H "Authorization: Bearer $CAMELAI_API_KEY" | jq -r '.[].id' ``` ```ts TypeScript const agent = await agents.upsert("quickstart", { model: "", // e.g. provider/model-id instructions: "You are a concise assistant.", tools: { weather }, }); ``` ```python Python agent = await agents.upsert( "quickstart", model="", # e.g. provider/model-id instructions="You are a concise assistant.", tools=[weather], ) ``` If a run fails with code `model_key_missing`, the agent names a model your account has no key for, and the error says which key to set: pick a model from the list, leave `model` out, or add the provider's key under **Models & keys**. ## With curl Without an SDK, an agent can't call tools in your process, but everything else works over REST. Create an agent with a first prompt, then poll the run: ```bash BASE=https://run.camelai.com; AUTH="Authorization: Bearer $CAMELAI_API_KEY" # The Idempotency-Key is the agent's key: the same key is the same agent. CREATED=$(curl -s $BASE/v1/agents -H "$AUTH" -H "Content-Type: application/json" \ -H "Idempotency-Key: quickstart-curl" \ -d '{"systemPrompt": "You are a concise assistant.", "prompt": {"text": "Write a haiku about durable agents."}}') AGENT=$(echo "$CREATED" | jq -r .id); REQ=$(echo "$CREATED" | jq -r .prompt.id) # ?wait=25 answers as soon as the run ends (or after 25 s, still running: ask again). until RUN=$(curl -s "$BASE/v1/agents/$AGENT/requests/$REQ?wait=25" -H "$AUTH") && jq -e '.state == "completed"' <<< "$RUN" > /dev/null; do :; done jq -r 'if .status == "failed" then "failed: \(.error)" else .outcome.result.reply end' <<< "$RUN" ``` Send later messages with `POST /v1/agents/{id}/prompt {"text": "..."}`, which answers `202` with the request to poll. See [REST API](/docs/camelrun/api). ## Stream the reply `run()` waits for the whole run. To show it as it happens, `stream()` yields text as the model writes it, each tool call and result, and the finished run last: ```ts TypeScript for await (const part of agent.stream("And tomorrow?")) { if (part.type === "text") process.stdout.write(part.text); if (part.type === "tool_call") console.log(`\n[${part.name}(${JSON.stringify(part.arguments)})]`); if (part.type === "done") console.log(`\n(${part.run.status})`); } ``` ```python Python stream = agent.stream("And tomorrow?") async for part in stream: if part.type == "text": print(part.text, end="", flush=True) elif part.type == "tool_call": print(f"\n[{part.name}({part.arguments})]") elif part.type == "done": print(f"\n({part.run.status})") run = await stream.result() # the same run as "done" has, also after a stream that broke off ``` The run's result is the source of truth, and the stream is for display. If a stream breaks, the run isn't lost: `run()` (or `stream.result()`) still resolves with it. ## What just happened * `upsert("quickstart", ...)` made a keyed agent, or found the one from last time and brought it to this configuration. It lives until you delete it, so running the file again continues the same conversation. * Because the agent has `tools`, this process served them. camelRun called `weather` here, over the connection the SDK holds. One process at a time serves an agent's tools, and only while it runs. Serverless functions, several web workers or task workers, and multi-user backends serve tools over HTTP instead, which also keeps them working through deploys; see [Several processes, workers and deploys](/docs/camelrun/tools#several-processes-workers-and-deploys). * `run()` resolved with a `Run`: `status` (`completed`, `input_required` or `failed`), `text`, `inputs`, `error`, `toolErrors` and `toolCalls` (Python `tool_errors`, `tool_calls`). A failed run throws a `RunError` unless you pass `throwOnError: false` (`throw_on_error=False`). A `completed` run can still list tool errors, e.g. `connection_lost` when the process serving its tools went away mid-call: check them. * `agents.close()` let the process exit. The agent stays in camelRun. One answer per input, with nothing to remember (classify, extract, vote)? Skip the agent: `await agents.run({ instructions, input, output })` is one call, and keeps nothing between runs. See [Stateless runs](/docs/camelrun/stateless-runs). ## Next steps ### [Concepts](/docs/camelrun/concepts) Keyed agents, runs, events, and where tools run. ### [Build a chat app](/docs/camelrun/chat-app) Put a streaming chat with each user's agent in your app. ### [Tools](/docs/camelrun/tools) Attached and served tools, identity, MCP and OpenAPI sources. ### [Production checklist](/docs/camelrun/production) What to check before you ship. --- --- title: 'Build an AI agent chat app with camelRun' sidebarTitle: 'Build a chat app' description: 'Put a streaming chat with each user''s camelRun agent in your React or Next.js app: one server route and one component, with your API key on your server.' --- [View as Markdown](https://camelai.com/docs/camelrun/chat-app.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) Your users talk to their own agents in your app. Replies stream in as the model writes them, tool calls show as cards (or as your own components), and questions and approvals are answered in place. Your API key stays on your server. For a short tour, see [deploying AI agents and chat apps with camelRun](https://camelai.com/use-cases/ai-agents). ## Start a new app ```bash export CAMELAI_API_KEY=art_... # https://run.camelai.com/console/tokens npm create @camelai/run-app my-app cd my-app && npm run dev ``` It writes the key from `CAMELAI_API_KEY` to the app's `.env.local`, so the key never lands in your shell history. This creates a Next.js app with the chat, the server route it talks to, a tool drawn by its own component, and a demo sign-in for you to replace with your own. Pass `--base-url` to point it at a self-hosted runtime. Examples with Vite (React, Vue, Svelte, Solid), the read proxy and the AI SDK are in [examples/frontend](https://github.com/qaml-ai/run/tree/main/examples/frontend). > **Warning:** > > The demo sign-in treats every browser as a user. Deployed with > `NODE_ENV=production`, it refuses everyone until you replace it in > `app/api/agent/route.ts` (or set `DEMO_AUTH=1`). ## Add it to an app you have ```bash npm install @camelai/run @camelai/run-react ``` ### Add the route One POST route does everything the browser needs. It checks who the user is with your own sign-in, gives their browser a short-lived token to read their agent, and sends their messages and answers. ```ts app/api/agent/route.ts import { createAgentHandler } from "@camelai/run/server"; export const POST = createAgentHandler({ // Reads CAMELAI_API_KEY from the environment, or pass apiKey. authorize: async request => { const user = await getUser(request); // your session check return user ? { userId: user.id, name: user.name } : null; // null: 401 }, agent: { instructions: "You help customers with their orders.", }, }); ``` The handler is a standard `fetch` handler, so it runs anywhere: ```ts // Hono app.post("/api/agent", c => handler(c.req.raw)); // Cloudflare Workers, Bun, Deno export default { fetch: handler }; // Express or node:http, mounted before any body parser for this route import { nodeListener } from "@camelai/run/node"; app.post("/api/agent", nodeListener(handler)); ``` ### Add the chat ```tsx "use client"; import { AgentChat } from "@camelai/run-react/ui"; import "@camelai/run-react/styles.css"; export default function Support() { return ; } ``` `` fills its container, so give the container a height. ### Route options | Option | | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `authorize(request, { thread, action })` | Required. Return `{ userId, name?, agentKey? }`, or `null` for a 401 | | `agent` | How the user's agent is made: `{ instructions, model, tools, definition, context, spendLimit, ... }`, or a function of the user | | `onSend({ auth, text, data, request })` | Runs before each message. Return `{ text?, metadata? }`, or `throw new Response(...)` to refuse it (quotas, moderation) | | `browserToken` | `{ ttlSeconds, events, redact, scopes, url }` for the tokens it mints. Default 15 minutes, with provider costs left out. `scopes: ["events", "state", "history", "inputs", "children"]` adds `children`, to read the agent's background sub-agents too | | `allowedOrigins` | Other origins whose pages may call the route, with CORS | | `proxy` | `true`: browsers also read their agent through this route and only ever talk to your origin. See [below](#read-through-your-route) | | `linkAnyMountedPath` | `true`: users may download any file in the agent's mounts. By default, only files the agent presented or wrote to its own workspace | | `apiKey`, `url` | Default `CAMELAI_API_KEY`, and `CAMELAI_BASE_URL` or `https://run.camelai.com` | ## How it stays secure * **The API key stays on your server.** The browser gets a [browser token](/docs/camelrun/browser) that reads one agent's events, history, state and inputs for 15 minutes. It can't send, answer, stop, or read another agent. The chat renews it through your route. * **Every write goes through your route**, after `authorize`. The browser never names an agent: the route picks the user's agent from what `authorize` returned, by default one per user and thread. * **Messages and answers are sent as the user** (`from: { id: userId, name }`), never as whatever the browser claims. * **Sends are idempotent.** Each carries the id the browser gave the message, so a retry never sends twice. * **Cross-site requests are refused.** The route only takes `application/json` and refuses `Sec-Fetch-Site: cross-site` unless you list the origin. * **Rendering is safe.** Markdown is rendered as elements, never raw HTML. Links are only http(s) and mailto, and images the model links are shown as links unless you set `allowImages`. ## Customize the UI ### AgentChat Replace any part of the prebuilt chat: ```tsx ``` Theme it with CSS variables on `.agent-chat`, such as `--agent-accent`, `--agent-radius`, `--agent-font` and `--agent-max-width`. With shadcn/ui, also import `@camelai/run-react/shadcn.css` to use your theme's tokens. ### Hooks Build your own UI on the same state: ```tsx import { AgentProvider, useMessages, useSend, useAgentStatus, useInputs } from "@camelai/run-react"; function MyChat() { const messages = useMessages(); // user bubbles and assistant turns, as typed parts const send = useSend(); const { isRunning, stop } = useAgentStatus(); const { inputs, answer } = useInputs(); return messages.map(message => ); } ``` Parts are `text`, `reasoning`, `tool` and `file`. Without React, the same store is `createAgentChat` from `@camelai/run/chat`, and Vue, Svelte and Solid have their own bindings. ### shadcn Copy the chat into your project, in your theme, and own the code: ```bash npx shadcn@latest add https://run.camelai.com/r/agent-chat.json npx shadcn@latest add https://run.camelai.com/r/agent-route.json # the Next.js route ``` ### AI SDK Use the AI SDK's `useChat` (and AI Elements) with the agent: ```tsx import { useChat } from "@ai-sdk/react"; import { lastAssistantMessageIsCompleteWithApprovalResponses } from "ai"; import { AgentRuntimeChatTransport } from "@camelai/run/ai-sdk"; const transport = useMemo(() => new AgentRuntimeChatTransport({ endpoint: "/api/agent" }), []); const { messages, setMessages, sendMessage, addToolApprovalResponse, stop } = useChat({ id: threadId, transport, sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithApprovalResponses, }); // Load the conversation so far from the agent. useEffect(() => { void transport.loadMessages({ chatId: threadId }).then(setMessages); }, [transport, threadId]); ``` The agent keeps the conversation, so only the new message is sent. ## Tools with their own UI Give the agent tools in the route, and draw their calls with your own components in the browser: ```ts route.ts import { schema, tool } from "@camelai/run"; agent: { tools: { get_weather: tool({ description: "The weather in a city", input: schema.Object({ city: schema.String() }), execute: async ({ city }, { identity }) => weather.now(city), // identity.subject is the user }), }, }, ``` ```tsx WeatherCard.tsx function WeatherCard({ args, state, result }: ToolRenderProps) { if (state === "input_streaming") return

Looking up {String(args.city ?? "")}...

; return ; } ``` A renderer gets `args` (partial while the model writes them), `state` (`input_streaming`, `running`, `input_required`, `done` or `error`), `result`, `progress`, and for a call that waits on the user, its `input` and `answer(value)`. > Tools given in the route run in that server process, so it must keep running > (`next start`, a container, a Node or Bun server). On serverless hosting, > serve the tools over HTTP with `serveTools` and name them in a definition: > `agent: { definition: "def_..." }`. See [Served tools](/docs/camelrun/tools#served-tools). ## Threads A `thread` is your name for one of the user's conversations. Each thread is its own agent, with its own history: ```tsx ``` The route receives it in `authorize(request, { thread })`. To let several people share one agent (a team's assistant), check access and return the key yourself: ```ts authorize: async (request, { thread }) => { const user = await getUser(request); if (!user) return null; if (thread && !(await isMember(user.id, thread))) return null; return { userId: user.id, name: user.name, agentKey: thread ? `team-${thread}` : undefined }; }, ``` Everyone in a shared thread sees the same conversation live, and it runs one turn at a time. Put private work in per-user agents. ## Read through your route By default the browser reads its agent's stream straight from camelRun with its browser token. With `proxy: true`, your route passes those reads through too, so the browser only talks to your origin and the runtime can stay private: ```ts app/api/agent/[[...path]]/route.ts const handler = createAgentHandler({ authorize, agent, proxy: true }); export const GET = handler; export const POST = handler; ``` Each open chat then holds a request on your server. Serverless platforms cut long requests at their time limit; the chat reconnects, and falls back to long polls of at most 25 seconds. Use the proxy when policy requires a single origin. ## The route's protocol The chat talks to your route in plain JSON, so anything that can POST can use it: a native app, a test, a script or curl. Each request is `POST ` with `Content-Type: application/json`, the cookie or header your `authorize` reads, and a body `{ action, thread?, … }`. `thread` (1 to 200 characters, or null) picks the conversation; with the user, it picks the agent (`agentKeyFor(userId, thread)`) unless `authorize` names one. | `action` | Body | Answer | | -------- | ------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `token` | | `{ token, expiresAt, agentId, url }`: a browser token that reads the agent (`/v1/agents/{id}/{events,history,state,inputs}` at `url`). With `proxy: true`, `{ proxy: true, agentId, token: "", expiresAt }`: read under the route instead. | | `send` | `{ text, clientId, data?, whileRunning?, wait? }` | `{ requestId, state, steeredInto? }`, as soon as the message is accepted. `clientId` (8 to 80 letters, digits, `_` and `-`) is the message's id: sending the same one again sends nothing twice. `whileRunning` is `queue` (the default) or `steer`. `data` reaches only your `onSend`. With `wait`, see below. | | `wait` | `{ requestId, wait? }` | A message sent before, by its `clientId`, as `send` with `wait` answers. Sends nothing. | | `answer` | `{ inputId, answer: { action, content? } }`, `action` being `accept`, `decline` or `cancel` | `{ input, request }`, as `POST /v1/agents/{id}/inputs/{inputId}` answers. The answer is the user's (`from`). | | `stop` | | `{ aborted: true }`: the agent's running turn is stopped. | | `link` | `{ path }` | `{ url, expiresAt }`: a download link for a file the agent presented or that is in its own workspace (any mounted path with `linkAnyMountedPath`). | With `proxy: true` the route also takes `GET [/threads/{thread}]/v1/agents/{id}/{events,history,state,inputs}` (the runtime's read routes, for the user's own agent only) and the file links that `link` gives. Errors are `{ error: { code, message } }` with the HTTP status: 401 `unauthorized` (`authorize` returned null), 400 `invalid_request` or `invalid_json` (the message names the field), 403 `forbidden_origin` (another site; see `allowedOrigins`), 405, 413 `too_large`, 415 `unsupported_media_type`, and the runtime's own errors passed on with their status and code (`INSUFFICIENT_CREDIT`, `not_found`, …; 502 `runtime_unreachable` when it cannot be reached). A `Response` that your `onSend` throws is answered as it is. ### Waiting for the reply The chat follows the agent's stream, so it sends without waiting. A caller without the stream passes `wait`: `true` (25 seconds) or whole seconds from 1 to 25. The route answers once the run ends, with `state: "completed"` and the run's `reply` (its final message's text), `error` (why it failed) or `stopped` (`input_required` or `spend_limit`). A run still going when the wait is over answers `state: "running"`: ask again with `wait`. ```bash curl -s https://app.example.com/api/agent -H "Cookie: session=…" -H "Content-Type: application/json" \ -d '{"action":"send","text":"Where is my order?","clientId":"msg_0001","wait":true}' # {"requestId":"msg_0001","state":"completed","reply":"It shipped yesterday…"} curl -s https://app.example.com/api/agent -H "Cookie: session=…" -H "Content-Type: application/json" \ -d '{"action":"wait","requestId":"msg_0001","wait":25}' ``` Each wait holds a request on your server: keep `wait` under your platform's limit (serverless functions cut requests at theirs). Without the handler, the runtime's `GET /v1/agents/{id}/requests/{requestId}?wait=25` does the same with your API key. ## Troubleshooting ### What does a 401 from the route mean? `authorize` returned `null`: the user isn't signed in, or the session cookie didn't reach the route. ### What if the browser says it could not reach the runtime (CORS)? camelRun allows any origin only for browser tokens on its read routes. Check the route's `browserToken.url`, or `CAMELAI_BASE_URL`. ### What if tool calls fail with "not connected"? The process serving the route's tools isn't running, which is typical on serverless hosting. Serve the tools with `serveTools` and a definition. ### Can I run several server instances? Tools given in the route belong to one process at a time. Run one instance, or serve tools over HTTP. --- --- title: 'camelRun concepts: durable agents, runs and tools' sidebarTitle: 'Concepts' description: 'How camelRun works: durable keyed agents, runs and events, forking, where tools run, and people in the loop.' --- [View as Markdown](https://camelai.com/docs/camelrun/concepts.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) ## Agents are durable and keyed An agent is a conversation that lasts. Its history, files, configuration (model, instructions, tools) and pending work live in camelRun, not in your process. Most agents are asleep at any moment and cost only storage. A message wakes one wherever camelRun has room, and it picks up where it left off. For a short tour, see [durable AI agents](https://camelai.com/run/durable-agents) and [AI agent memory](https://camelai.com/run/agent-memory) on camelRun. You name an agent with a **key**, your own id for it, such as `support-triage`, `user-123` or `thread-9f2c`. Keys are 1 to 80 letters, digits, `_` and `-`. ```ts const agent = await agents.upsert("user-123", { model, instructions, tools }); ``` * The same key is the same agent, from any process, until you delete it with `agent.delete()` (`DELETE /v1/agents/{id}`). Only your API key can delete an agent. Its own token gets a 401, because deleting purges the agent's history and workspace. * `upsert` makes the agent if there is none, and otherwise brings it to the configuration you pass. A changed model, instructions or tool list applies between turns, and the history carries over. * Who the agent acts for (`subject`, `context`), its definition and its mounts are fixed when it is made. Changing them is a 409 that names the field. Delete the agent and upsert again to start over. An upsert may change the mounts with `remount: true`. * A deleted key makes a fresh agent the next time, with a new id and token. * Make an agent once per user, conversation or job, and reuse it. Creating agents is cheap and limited only against abuse (600 a minute; an upsert that changes nothing doesn't count). The limit to design around is **busy agents**: how many may run at once, 20 on free credit and more with each [usage tier](/docs/camelrun/limits#usage-tiers). Runs are limited to 240 a minute on free credit, 600 once you buy credit. See [Limits](/docs/camelrun/limits). * Over REST, the key is the `Idempotency-Key` header of `POST /v1/agents`. Every agent also has an **id** (`client_...`), which is safe to log and store, and a **token**, which lets its holder run that agent and nothing else (not delete it, nor change who it acts for, its tools' sources or its limits). You rarely need the token, because upserting by key gives you the agent again. If a token leaks, `POST /v1/agents/{id}/credentials/rotate` gives the agent a new one. The old one stops working at once, and connections made with it close. Agents made without a key are scratch agents that expire after a day unless you set `ttlSeconds`. `idleTtlSeconds` instead makes an agent live that long from its latest run, so an agent in use is kept and one left alone expires (say, a chat that should go once nobody has written in a week). Agents can be grouped into [workspaces](/docs/camelrun/workspaces), each with its own spend and usage. ## Runs A message to an agent starts a **run**: the model loop, with every tool call it makes, until the model answers or something stops it. An agent has one run at a time. A message sent during a run waits for it (`whileRunning: "queue"`, the default) or joins it (`whileRunning: "steer"`, read by the running turn after its current step). A steer is answered at once: `agent.steer()` returns a receipt, `accepted`, `taken` or `queued`. ```ts const run = await agent.run("Summarize ticket 123", { user: "alice" }); ``` | Field | | | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `id` | The run's id. Pass `idempotencyKey` to choose it; sending the same key again returns the same run | | `status` | `completed`, `input_required` (it waits on a person, see `inputs`), or `failed` | | `text` | The final reply | | `output` | With `output: schema`, the answer in that shape. See [Structured output](/docs/camelrun/structured-output) | | `inputs` | What it waits on, each with `answer()` | | `error` | `{code, message, uncertain?}` when it failed | | `toolErrors` (Python `tool_errors`) | Tool calls that didn't complete (timed out, connection lost, nobody serving the tools). The model was told and carried on | | `toolCalls` (Python `tool_calls`) | Every tool call the run made (the first 100), calls from `js_exec`'s code included: `{tool, toolCallId, innerCallId?, ok, code?, agentId?}`. A `delegate` call names its sub-agent (`agentId`). Arguments and results are in history | | `files` | Files the run wrote | | `usage` | What its model calls used. `subagentCostUsd` is what its sub-agents spent, and `imageCostUsd` what its `generate_image` calls cost | `run()` has no timeout: runs can take minutes, and a run waiting on a person can wait for days. Pass an `AbortSignal` (Python: `timeout=`) to stop waiting; the run itself goes on. `agent.abort()` stops it: the running turn ends, and the runs queued behind it are cancelled (code `cancelled`). `abort({ queued: "keep" })` stops the running turn only. A failed run throws a `RunError` carrying the run, unless you pass `throwOnError: false`. ### Runs without an agent Every run of an agent sees its history; long conversations are compacted automatically. When there is no agent to keep at all (classify, extract, vote, judge), `agents.run({ instructions, input, output })` (REST: `POST /v1/runs`) takes a configuration and an input and returns the result, with no agent made. See [Stateless runs](/docs/camelrun/stateless-runs). ### Long conversations An agent's history can outgrow its model's context window, so camelRun **compacts** it: older messages are summarized, and the model sees the summary followed by the recent messages. History keeps every message; only what the model is sent changes. Compaction runs in the background. Once the context comes within a margin of its limit, the summary is made after the run ends, or between model requests, while the agent goes on. Only a run whose context wouldn't fit waits for a summary. Summaries are billed like model responses (`kind: "compaction"` in usage) and count against spend limits. ## Forking A **fork** is a new agent made from an existing one: the same configuration (model, instructions, definition, tools, built-ins, who it acts for, key scope, spend and run limits), a copy of its history, and a copy of its workspace files. From then on each is its own: a turn, a summary or a file written in one never shows in the other. Fork to try another direction without losing the first, to branch a conversation where a user edits an earlier message, or to start many agents from one prepared one. ```ts const branch = await agent.fork({ key: "thread-9f2c-b" }); const earlier = await agent.fork({ atMessage: 4 }); // history through message 4 ``` ```python branch = await agent.fork(key="thread-9f2c-b") earlier = await agent.fork(at_message=4) ``` * **Where it ends.** By default the copy ends with the agent's last finished turn, never inside one that is running or waiting on a person, so forking a busy agent is safe. `atMessage` ends it earlier: a history index keeps that message and the tool results that answer it, and a run's request id keeps that run's whole turn. A message in a turn that hasn't ended is a 409 `FORK_POINT_RUNNING`. * **Compaction.** The copy keeps the source's summaries, so the fork's model sees what the source's would at that point. History keeps every message. * **Files.** The fork's `/workspace` is a fork of the source's volume as it is when you fork, not as it was at `atMessage`. Other volumes the source mounts stay shared. Files its history refers to stay available to the fork, whatever becomes of the source. * **Not copied:** schedules, channel bindings, pending inputs, runs and their events, and what the source has spent. * **Its own:** `subject`, `context`, `systemPromptAppend` and `modelHeaders` can be given for the fork instead of the source's, so a fork for another conversation names that conversation. * **Keys.** A fork made with a `key` lives until deleted, and forking again with the same key returns the same fork. Without one it's a scratch agent that lives a day, unless you set `ttlSeconds`. * `forkedFrom: {agentId, atMessage}` on the fork (`GET /v1/agents/{id}`, the console) says where it came from. A fork is an agent like any other: it counts toward your agents and storage. It also counts against the agent-create rate limit (`atMessage: 0` included); to run one message without any history, use a [stateless run](/docs/camelrun/stateless-runs) instead. The copy itself costs only storage, and shares stored content with the source. Over REST it's `POST /v1/agents/{id}/fork` with `{key?, name?, atMessage?, ttlSeconds?, subject?, context?, systemPromptAppend?, modelHeaders?}`; in the console, **Fork** on the agent's page, or **Fork from here** under a reply. Browser tokens can't fork. ## The result is the truth; events are for display An agent's **events** stream every step: text as the model writes it, tool calls and their progress, and inputs asked and answered. Use them to show the agent working, not to decide what happened. * The run's result (`run()`, the `run.completed` webhook, or `GET /v1/agents/{id}/requests/{requestId}`) is recorded durably and always arrives, even if your process restarts or the connection drops. * The stream replays from memory on reconnect, as far back as it can. Where it can't (camelRun restarted, a long disconnection), the SDK gets a snapshot of the running turn instead. Reply to your user from the run, and render the stream as progress. See [Events](/docs/camelrun/events). ## Where tools run Pick by where your code runs: | Your situation | Use | How | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | A long-lived server or worker, and the tools touch its state | **Attached tools** | `tools` on `upsert`. The SDK holds a connection and camelRun calls your functions over it. One process serves an agent's tools at a time. Closing it on SIGTERM finishes its calls, so deploys lose none | | Serverless functions, several instances, or one backend serving many users' agents | **Served tools** | `serveTools(tools)` on an HTTPS endpoint of yours, named in a definition with `auth: { type: "runtime" }` (or in the agent's own `mcpServers`). camelRun calls it with a signed token saying who each call is for. Safe through your deploys and restarts | | A third-party API | **OpenAPI** or **MCP** sources in a definition | camelRun calls the API itself, with credentials it stores sealed | | Web search, fetching pages, scheduling, asking the user | **Built-ins** | `builtins: ["web_search", "web_fetch", "schedule", "ask_user"]` | | Other agents doing tasks for this one | The **`delegate`** built-in | `delegate: { agents: ["researcher"] }`. See [Multi-agent](/docs/camelrun/multi-agent) | | Many agents, coordinated by a plan | The **`workflow`** built-in: the model writes a script that calls agents | `workflow: { agents: ["researcher"] }`, and `run_workflow`. See [Workflows](/docs/camelrun/workflows) | The same `tool({...})` definitions work attached and served, so you can start attached and move to served without changing a tool (see [Several processes, workers and deploys](/docs/camelrun/tools#several-processes-workers-and-deploys)). Every agent also has file tools over its [files](/docs/camelrun/files), and `js_exec`, a sandbox where the model writes JavaScript that calls its tools. See [Tools](/docs/camelrun/tools). ## Processes and connections A process that serves an agent's tools **attaches** to it: it holds the agent's event stream and answers its tool calls. Only one process at a time can, so a tool call never lands in the wrong place. A second process that tries gets `APPLICATION_CONNECTED`, unless it passes `takeover: true`; the one it replaces hears `APPLICATION_REPLACED` and goes on without serving the tools. Any number of processes can **run** an agent without serving its tools: an agent upserted without local tools, or with `attach: false`, follows the stream read-only. That's how serverless functions and webhook handlers run agents whose tools are served elsewhere. Such a handle connects lazily: it holds the stream only while `agent.stream()` reads a run, so a server holding many agents holds no idle connections. A run of an agent with attached tools while no process serves them is refused with `APPLICATION_NOT_CONNECTED`, after a few seconds' grace. Pass `allowDisconnected: true` to run anyway; calls to those tools then fail and show in `toolErrors` as `not_connected`. Schedules, channels and resumed runs are never refused, so agents that must work with nobody attached should use served tools. ## People in the loop A run can stop and wait for a person: the model asks a question (`ask_user`), a tool needs approval before it runs, or a tool asks for a form. The run ends with `status: "input_required"` and its `inputs`, the agent sleeps, and answering the last input resumes it. ```ts if (run.status === "input_required") run = await run.inputs[0].answer(true, { from: "alice" }); ``` See [Human input](/docs/camelrun/human-input). ## Idempotency Everything that changes something can be retried safely: * `upsert` by key, and `run` or `POST /v1/agents/{id}/prompt` by `idempotencyKey` (the request id). The same key returns the same agent or run. * Every other POST takes an `Idempotency-Key` header. A retry with the same key, path and body gets the first answer again for a day. The same key with a different body is a 409. An answer with a secret shown once (an API token, a signing secret, a browser token, a rotated agent token or a signed link) is never stored: a retry of a key that succeeded is a 409, not the secret again. * Every tool call carries `context.idempotencyKey`, the same for every attempt of that call: a retry after a lost connection, or the call run again after a person answered. Key your side effects by it. (`context.callId` is new each attempt.) A tool call whose answer was lost (the connection dropped, or its deadline passed) is never sent again. The model is told its outcome is unknown, so it can check before repeating anything. ## Waking later An agent can be woken by a schedule (`agent.schedule({ text, inSeconds, at, everySeconds, calendar })`, or the `schedule` built-in so the agent sets its own), by a [channel](/docs/camelrun/channels) (Slack, Telegram, Discord), or by your own code from a [webhook](/docs/camelrun/webhooks). See [Schedules](/docs/camelrun/schedules) for calendar schedules and report emails. A repeating schedule's wake-ups are turns of the agent, so each one reads the history before it, compacted as it grows. That lets a daily agent build on what it did before. A wake-up camelRun can't deliver (the agent is busy at its account's limit, say) is tried again 1, 5 and 15 minutes later, then given up. A refusal a retry can't change, like a spend limit (402), is given up at once. A repeating schedule then moves to its next occurrence and lists the failure (`failures` and `lastError`) until a delivery succeeds. A one-off schedule given up is removed. ## Definitions A **definition** is a reusable configuration: model, instructions, tool sources (MCP servers, OpenAPI specs, built-ins), limits and mounts. Make agents from one with `definition: "def_..."` on `upsert`, and roll a change out to all of them with `apply: "all"`. See [Definitions](/docs/camelrun/definitions). ## Several agents Agents can work together. With the `delegate` built-in, an agent hands a task to a **sub-agent** (an agent of its own, made from a definition for the task, or an existing one) and gets its answer back as the call's result; several at once run in parallel. A sub-agent counts against its parent's limits. There are no handoffs: the conversation stays with the agent that has it. See [Multi-agent](/docs/camelrun/multi-agent). --- --- title: 'camelRun pricing: agent time, storage and credit' sidebarTitle: 'Pricing and credit' description: 'camelRun is prepaid: $0.01 per hour an agent spends in a run, $0.10 per GB-month stored, and tokens at the provider''s price, or nothing on your own keys.' --- [View as Markdown](https://camelai.com/docs/camelrun/pricing.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) camelRun is prepaid: you buy credit, and usage draws it down. There is no subscription and no per-seat or per-agent fee. `GET /v1/billing` (the console's **Billing** page) shows your balance, this month's spending by kind, and the rates below as your account is charged them. | What | Price | | ------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Model calls on the platform's keys | The provider's own reported cost, else the model catalog's price (`GET /v1/models`, `cost` per million tokens). No markup. OpenRouter models add what funding OpenRouter credit costs: ×1.055 | | Model calls on your own key (or a key scope's) | Nothing from camelRun: the provider bills you | | Agent time | $0.01 per hour an agent spends in a run (model calls and tool execution), metered continuously. An idle agent costs nothing | | Storage | $0.10 per GB-month of what your agents and volumes keep (history, files, snapshots), charged daily | | `web_search` on the platform's keys | Per search, by the provider that answered: Exa $0.007, Brave $0.005, Parallel $0.001 | | `web_fetch` page rendering on the platform's key | $0.00083 per page Firecrawl renders | | `x_search` on the platform's xAI key | What xAI reports for the call (Grok tokens and the posts and profiles it fetched), with no added search fee | | [Transcription](/docs/camelrun/voice) on the platform's OpenAI key | $0.0045 per minute of audio (`gpt-transcribe`), billed per second | | [Voice calls](/docs/camelrun/voice-calls) on the platform's key | The realtime model's tokens at its list price (about $0.02 to $0.05 a conversational minute); what people say transcribed at the transcription price | | [Speech](/docs/camelrun/voice) (`POST /v1/speech`) on the platform's OpenAI key | $0.015 per minute of speech made (`gpt-4o-mini-tts`), billed per second | | Speech through other providers, on the platform's key for each | Their list prices, per 1,000 characters spoken: ElevenLabs Flash/Turbo v2.5 $0.05, Cartesia `sonic-3` $0.05, Deepgram `aura-2` $0.03 | | [Images](/docs/camelrun/images) on the platform's OpenAI key | Per token as OpenAI bills `gpt-image-2.5-flare`: $5 per million text tokens in, $8 per million image tokens in, $30 per million image tokens out. A 1024x1024 image is $0.0059 at `low`, $0.0132 at `medium`, $0.0527 at `high` | | [Computers](/docs/camelrun/computers) | Nothing from camelRun: the provider (Modal, Daytona, E2B, Browserbase and the rest) bills your account with it | ## Credit * **Starting credit: $5**, once. GitHub accounts older than 30 days get it at sign-up. When the **Billing** page offers it, everyone else can unlock starting credit by verifying a card; the card is not charged. A card unlocks credit once, across all accounts, and prepaid cards unlock none. * **Buying credit:** $5 to $1,000 at a time, with a 5.5% fee on the purchase. Auto top-up can buy more when your balance runs low. * **Before your first purchase:** at most $1 of model tokens and agent time an hour, and 1 GB stored. After it, 100 GB stored, and the [usage tiers](/docs/camelrun/limits#usage-tiers) raise how many agents may be busy at once as your purchases add up. * At zero balance, new runs and file writes get 402 `INSUFFICIENT_CREDIT`, and a running turn ends after the response that spent the last credit. Nothing is deleted: add credit and send the next message. ## Spending less * Use your own provider keys (`PUT /v1/providers/:provider/key`): model calls then cost you only what the provider charges, and camelRun charges agent time and storage. See [Models and keys](/docs/camelrun/models-and-keys). * Bound an agent or a run with a [spend limit](/docs/camelrun/models-and-keys#spend-limits), and see where credit goes with `GET /v1/usage` or the `usage.recorded` [webhook](/docs/camelrun/webhooks). * Charging your own users? [End-user credits](/docs/camelrun/end-user-credits) give each person a balance and daily allowances, and can mark up what their agents spend. ## Usage by workspace Group agents into [workspaces](/docs/camelrun/workspaces) to see spend by project. The console's **Usage** page follows the workspace switcher, and `GET /v1/usage?workspaceId=ws_...` returns one workspace's usage (`workspaceId=unassigned` for the rest; omit it for account totals and a `workspaces` summary). A workspace's costs include model calls and recorded tools such as `x_search`, `web_search` and images. Agent time and storage are charged to the account and not split by workspace. Each cost stays with the workspace its call was made in, even after the agent moves. A self-hosted runtime charges nothing unless its operator sets up billing; see [Self-hosting](/docs/camelrun/self-hosting). --- --- title: 'camelRun agent tools: your code, MCP and OpenAPI' sidebarTitle: 'Tools' description: 'Give camelRun agents tools: functions in your own process or served over HTTP, remote MCP servers, OpenAPI specs, built-ins such as web search, and code.' --- [View as Markdown](https://camelai.com/docs/camelrun/tools.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) An agent's tools are how it acts. This page covers where tools run, how to write them so a retry never does harm, and how camelRun tells a tool who a call is for. For the short version, see [Where tools run](/docs/camelrun/concepts#where-tools-run). For native Linux computers and remote browsers, see [Computers](/docs/camelrun/computers). ## Write a tool ```ts TypeScript import { schema, tool } from "@camelai/run"; const refund = tool({ description: "Refund an order in full", input: schema.Object({ orderId: schema.String() }), timeoutMs: 60_000, execute: async ({ orderId }, context) => { context.progress("Contacting the payment provider"); return payments.refund(orderId, { idempotencyKey: context.idempotencyKey, signal: context.signal }); }, }); ``` ```python Python from camelai_run import ToolContext, tool @tool(timeout=60) async def refund(order_id: str, context: ToolContext) -> dict: """Refund an order in full""" context.progress("Contacting the payment provider") return await payments.refund(order_id, idempotency_key=context.idempotency_key) ``` * **Input.** TypeScript takes a [TypeBox](https://github.com/sinclairzx81/typebox) schema (`schema` is TypeBox's `Type`) and infers the argument types from it. Python derives the schema from the annotations and the description from the docstring. Arguments are checked before your function runs. * **Result.** Return any JSON value; `undefined`/`None` is sent as `null`. Throw (or raise) to tell the model the call failed; it sees your message. A result is at most 1 MiB of JSON. The model reads at most 32,000 characters; a longer result is saved whole (up to 700 KB) to `/workspace/tool-results/.txt` so it can read the rest. * **Python functions** may be `async` or plain. A plain one runs in a thread, so it never blocks the connection. * **`context.idempotencyKey`** is the same for every attempt of one call: a retry after a lost connection, or the call run again after a person answered. Pass it to anything with side effects. `context.callId` is new on each attempt. * **Deadline.** A call may go 15 seconds without answering by default (`timeoutMs`, or `@tool(timeout=)`, from 1 second to 20 minutes). Each `context.progress(...)` restarts the wait, up to 20 minutes in all, and people watching the agent see the progress. A call cut off is reported to the model as "outcome unknown" (it may still finish in your process), so make the side effect idempotent and the model can check and retry safely. * **`context.signal`** aborts when camelRun cancels the call (the run was aborted, or the deadline passed). Cancellation is not rollback. * **`context.identity`** says who the call is for. See [Identity](#identity). * **`needsApproval: true`** (`needs_approval=True`) makes a person approve each call first, and `context.confirm`, `context.ask` and `context.requireUrl` ask from inside the tool. See [Human input](/docs/camelrun/human-input). * **`exposure`** (`@tool(exposure=...)` in Python) is `"direct"` (the model calls it as a tool), `"codemode"` (only from code in `js_exec`, as `tools.(args)`), or `"both"`. By default a source of up to 10 tools is `both` and a larger one is `codemode`, so big catalogs don't crowd the model's context. **So by default the model can call your tools from `js_exec`**, and it may well batch them in code. ### Keep a tool out of js\_exec A tool called from code runs inside `js_exec`'s call: its arguments don't stream as `toolcall_delta` events, and a page that renders a tool's arguments as they are written sees nothing until the code has run. To keep a tool out of `js_exec`, make it `direct`: | Where the tool comes from | Set | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Your process (`tools` in `upsert`) | `exposure: "direct"` on the tool; Python `@tool(exposure="direct")` | | A served or MCP server named in a definition | `"exposure": "direct"` on the server entry (every tool), or `_meta["agent-runtime/exposure"]` on a tool in its `tools/list` (that tool; it beats the server's) | | An OpenAPI source | `"exposure": "direct"` on the source | A tool with `needsApproval` is always direct. The console's agent page shows each tool's exposure (**Tools the model gets**). `js_exec` itself stays, with `fs`, `fetch` and the tools left in it. ## Attached tools Pass `tools` to `upsert` and this process serves them: ```ts const agent = await agents.upsert("ops", { model, instructions, tools: { refund } }); ``` The SDK holds the agent's event stream, camelRun sends each tool call over it, and the SDK posts the result back. Your tools run with your process's state and credentials, and nothing is exposed on the network. * One process at a time serves an agent's tools. Another process that upserts with the same tools gets `APPLICATION_CONNECTED`. Pass `takeover: true` to replace the first, or `attach: false` to run the agent without serving them. * A run while no process serves the tools is refused with `APPLICATION_NOT_CONNECTED`, unless you pass `allowDisconnected: true` (`allow_disconnected=True`): then calls to them fail, listed in the run's `toolErrors` with code `not_connected`. Runs a schedule or a channel starts are never refused and may find nobody connected, so use served tools for those agents. * If the connection drops mid-call, the model is told the outcome is unknown, and the call is never sent again. An existing MCP SDK server can be attached as is: `upsert(key, { mcp: await fromMcpServer(server) })`, from `@camelai/run/mcp`. ### Several processes, workers and deploys Every process that upserts an agent with `tools` tries to serve them, and only one can. Under uvicorn or gunicorn with several workers, Celery, or any fleet of instances, the first to connect serves the tools and the others fail with `APPLICATION_CONNECTED` (or take them from each other, with `takeover`). Pick one of these: * **Served tools (recommended).** Serve the tools over HTTP from your web app (`serveTools` / `serve_tools`, below) and name the server in a definition. Every process, web worker and task then upserts the agent from the definition with no `tools`, and any of them can run it. Served tools are deploy-safe: each call is an HTTP request of its own to whichever instance is up, so a rolling deploy that drains its connections loses no call. * **One tool process.** Run one long-lived process that upserts the agent with its `tools` and stays up. Everywhere else, upsert it with the same `tools` and `attach: false` (`attach=False`), which runs the agent without serving them. #### Deploy a tool process Close the agents when the process is told to stop, and a deploy loses no call: ```ts TypeScript process.once("SIGTERM", () => void agents.close().then(() => process.exit(0))); ``` ```python Python from contextlib import asynccontextmanager from fastapi import FastAPI @asynccontextmanager async def lifespan(app): yield await agents.close() # uvicorn runs this on SIGTERM app = FastAPI(lifespan=lifespan) ``` `close()` tells camelRun this process takes no new calls, finishes the calls it is running (for up to 25 seconds: `close({ drainMs })`, `close(drain=)` in seconds), then disconnects. Meanwhile new calls go to the next process. In a rolling deploy, start it with `takeover: true` (`takeover=True`), which takes the tools at once while the old process finishes its calls. In a stop-then-start deploy, it connects without a takeover, and calls wait a few seconds for it. A takeover alone (without `close()`) also lets the replaced process answer the calls it has, for up to 30 seconds. A deploy can still lose a call that outlives the drain, and every call running when a process crashes or is killed without `close()`. The model is then told the tool's server disconnected during the call and it may or may not have taken effect, and the run lists it in `toolErrors` (Python `tool_errors`) with code `connection_lost`. Key side effects by `context.idempotencyKey` so that checking and retrying is safe. #### When the tool process dies mid-run The run lives in camelRun, not in your process, so it goes on without it: * **The call that was running** fails as `connection_lost`, as above. The model is told, and usually checks or tries again. * **A call while no process serves the tools** waits 3 seconds for one to connect, then fails as `not_connected` (it didn't run). * **The restarted process serves the run's next calls.** Once it upserts the agent with its `tools`, it serves every call that comes, including calls of runs a process before it started. A run isn't tied to the process that sent it. * **The `run()` the dead process awaited is gone, but its run isn't.** Give each run an id of yours (`idempotencyKey`), saved before you send it, such as the job or order it's for. After the restart, `run()` with the same text and key answers with that run instead of starting another: its result if it has ended, or once it ends if not. Without an SDK, `GET /v1/agents/{id}/requests/{key}?wait=25` answers the same, and `GET /v1/agents/{id}/state` lists the agent's runs. Check `toolErrors` even when the run's `status` is `completed`: a run whose call was cut off often ends normally, after the model worked around it. ```ts TypeScript // On startup (and after a crash): serve the tools, then collect the job's run. const agent = await agents.upsert("ops", { model, instructions, tools: { refund } }); const run = await agent.run(`Refund order ${job.orderId}`, { idempotencyKey: job.id }); const lost = run.toolErrors.filter(error => error.code === "connection_lost"); if (lost.length) console.warn(`check whether ${lost.map(error => error.tool).join(", ")} took effect`); console.log(run.status, run.text, run.toolCalls.map(call => call.tool)); ``` ```python Python # On startup (and after a crash): serve the tools, then collect the job's run. agent = await agents.upsert("ops", model=model, instructions=instructions, tools=[refund]) run = await agent.run(f"Refund order {job.order_id}", idempotency_key=job.id) lost = [error for error in run.tool_errors if error["code"] == "connection_lost"] if lost: print("check whether", ", ".join(error["tool"] for error in lost), "took effect") print(run.status, run.text, [call["tool"] for call in run.tool_calls]) ``` ## Served tools A server of yours answers tool calls over HTTP (MCP's Streamable HTTP), and a definition tells camelRun to call it. Any number of instances can serve, nothing needs to stay connected, and camelRun signs a token for each call saying which agent it is for, whom the agent acts for, and who is acting. ```ts TypeScript import { serveTools } from "@camelai/run/server"; // A fetch handler: Cloudflare Workers, Bun and Deno serve it as is. // tenant: your tenant's id, from GET /v1/me or `await agents.runtime.me()`. export default { fetch: serveTools({ refund }, { runtime: "https://run.camelai.com", tenant: "acme" }), }; ``` ```python Python # pip install "camelai-run[server]" from camelai_run import serve_tools # An ASGI app: run it with uvicorn, or mount it in FastAPI. app = serve_tools([refund], runtime="https://run.camelai.com", tenant="acme") ``` Then name the server in a definition and make agents from it: ```ts const definition = await agents.runtime.upsertDefinition("support", { name: "Support", mcpServers: [{ name: "shop", url: "https://tools.example.com/mcp", auth: { type: "runtime" } }], }); const agent = await agents.upsert(`user-${user.id}`, { definition: definition.id, subject: user.id, context: { org: user.orgId }, }); ``` Every request without a valid token gets a 401. `serveTools` checks the signature against camelRun's published Ed25519 keys, plus the issuer, your tenant, the audience (your server's URL) and the expiry. Nothing per user is stored anywhere. > **Warning:** > > `tenant` is required, and it matters. Any camelRun account can make agents > with any `subject` and point them at your URL, and camelRun signs their tokens > too. Only the tenant check tells your agents from theirs. Pass your tenant's > id, or a list if several of your tenants share the server. To offer each agent its own tools (an admin's, a plan's), pass a function of the caller's identity instead of the tools. A tool it isn't given is refused if called. ```ts TypeScript serveTools(identity => identity.context.role === "admin" ? { refund, ban } : { refund }, options); ``` ```python Python serve_tools(lambda identity: [refund, ban] if identity.context.get("role") == "admin" else [refund], runtime=..., tenant=...) ``` * **On Node,** wrap the handler: `createServer(nodeListener(handler, { origin: "https://tools.example.com" }))`, with `nodeListener` from `@camelai/run/node`. Set `origin` to your public URL behind a proxy that ends TLS. `trustProxy: true` reads `X-Forwarded-*` instead; only use it where the proxy overwrites those headers. * **In Python,** behind a proxy, run uvicorn with `--proxy-headers` (and `--forwarded-allow-ips` naming the proxy), or pass `audience="https://tools.example.com/mcp"`. * **Django, Flask or any WSGI server:** `camelai_run.sync.serve_tools` takes the same arguments and is a WSGI app. Plain functions run in the request's thread, with its database connection. Mount it at the path the definition names, and behind a proxy pass `audience`. `camelai_run.sync.verify_runtime_token` checks a token in a view of your own. ```python # Flask from werkzeug.middleware.dispatcher import DispatcherMiddleware from camelai_run.sync import serve_tools app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {"/mcp": serve_tools([refund], runtime="https://run.camelai.com", tenant="acme")}) ``` * **Built with the MCP SDK** or Cloudflare's `createMcpHandler`? Verify with `runtimeAuth(request, { runtime, tenant })`, pass the result as the request's `auth`, and read `runtimeIdentity(extra)` in a handler. `verifyRuntimeToken(token, { runtime, tenant, audience })` checks a token on its own. * **In tests,** `testRuntime()` (`@camelai/run/testing`, Python `TestRuntime()`, `camelai_run.sync.TestRuntime()` for a WSGI app) signs tokens with its own key, for tenant `test`, which `rt.options` passes: ```ts const rt = await testRuntime(); const result = await rt.callTool(serveTools(tools, rt.options), "https://app.test/mcp", "refund", { orderId: "o1" }, { subject: "alice" }); ``` ### A server that moves By default a token's audience is the server's URL, so a server that moves to a new domain or path refuses the tokens of agents still calling the old URL. Two ways to move without that: * Give the server a name that doesn't change: `audience: "urn:camelrun::"` in the definition's server entry (your tenant's id, as `GET /v1/me` shows it, then a name of letters, digits and `._~:-`), and the same `audience` to `serveTools`. Only your tenant's sources can name an audience in your tenant's namespace. * Accept both addresses while agents move: `audience` takes a list, such as `["https://old.example.com/mcp", "https://new.example.com/mcp"]`. Agents keep the definition revision they were made from, so after changing the server's `url`, apply the new revision to them (see [Definitions](/docs/camelrun/definitions#roll-out-a-change)). ```ts // Definition: { name: "shop", url: "https://tools.example.com/mcp", auth: { type: "runtime" }, audience: "urn:camelrun:acme:shop" } export default { fetch: serveTools({ refund }, { runtime: "https://run.camelai.com", tenant: "acme", audience: "urn:camelrun:acme:shop" }) }; ``` ## Identity Every tool call carries `context.identity`. Served tools get it from the signed token and attached tools from the call itself, so a tool reads it the same way either way: | Field | | | ------------------------------- | ---------------------------------------------------------------------------------- | | `user` | Whom to authorize as: the turn's actor, else the agent's subject | | `subject` | Whom the agent acts for, set when it was made (its id if none) | | `actor` | Who is acting in this turn: the run's `user`, or an `actor` given with the message | | `context` | Claims given when the agent was made, such as `{ org, workspace }` | | `tenant`, `agent`, `definition` | Whose agent it is, and which one (`definition` only for an agent made from one) | | `origin` | Where the turn came from: a channel, its conversation and sender | | `approval` | The person's approval, for a call that needed one | `subject` and `context` are set with your API key when the agent is made. The agent's own token can't change them, and the model can't touch any of it. Once the token is checked to be from your own tenant, a tool can trust `identity`, and should never take a user id from the model's arguments. See [Many users](/docs/camelrun/multi-user). ### Verify the token yourself For servers that verify by hand, for example with `jose`: ```json { "iss": "https://agents.camelai.dev", "aud": "https://tools.example.com/mcp", "sub": "u_123", "tenant": "acme", "agent": "client_...", "definition": "def_...", "ctx": { "org": "acme" }, "act": "u_456", "origin": { }, "req": "run_...", "tcid": "toolu_...", "par": "client_...", "root": "client_...", "iat": 1790000000, "exp": 1790000120, "jti": "..." } ``` ```ts const jwks = createRemoteJWKSet(new URL("https://run.camelai.com/.well-known/jwks.json")); const { payload } = await jwtVerify(token, jwks, { issuer: "https://agents.camelai.dev", audience: "https://tools.example.com/mcp", algorithms: ["EdDSA"], }); // Any tenant's agent can get a token for your server's URL: accept only your own tenant's. if (payload.tenant !== "acme") throw new Error("Not our tenant's agent"); ``` `iss` is `https://agents.camelai.dev`, the hosted runtime's first address, at either of its URLs; the SDKs expect it for both. Tokens live two minutes and each request gets its own `jti`. `aud` is the server's URL, or the source's `audience`, so a token can't be replayed against another server. Check `tenant` yourself. * `act` and `origin` are absent outside a turn (listing a server's tools as an agent starts), and so are `req`, the run's request id, and `tcid`, the model's tool call (`identity.requestId` and `toolCallId` in the SDKs). * `par` and `root` are set only in a [sub-agent](/docs/camelrun/multi-agent)'s run: the agent that started it and the first agent of its chain (`identity.parentAgentId` and `rootAgentId` in the SDKs). Authorize a sub-agent's call by `root` when only the first agent's user is yours. * The keys are at `/.well-known/jwks.json` (cache them for minutes). ## MCP servers A [definition](/docs/camelrun/definitions) can list MCP servers that camelRun calls itself: ```json {"name": "Support", "mcpServers": [{ "name": "kb", "url": "https://mcp.example.com/mcp", "auth": {"type": "bearer", "token": "..."}, "headers": {"X-Team": "support"}, "allowTools": ["search", "fetch_article"], "exposure": "both", "timeoutMs": 30000 }]} ``` * Streamable HTTP, falling back to the older SSE transport when a server answers its POST with 400, 404 or 405. * `auth` is `{"type": "bearer", "token"}`, `{"type": "runtime"}` (signed identity tokens) or `{"type": "exchange", "url"}` (a short-lived credential fetched from an endpoint of yours; see [Fetched credentials](#fetched-credentials)). `headers` and a bearer token are stored sealed; the API returns only `headerNames` and `auth` without a token. Updating a server without them keeps its stored credentials, unless its URL moved to another origin. * `fileArguments` (`"on"` or `"off"`) says whether its tools are sent the agent's files: on by default with `auth: {"type": "runtime"}` only. See [Files in tool calls](#files-in-tool-calls). * Tools reach the model as `__`, filtered by `allowTools` and `denyTools`. A server can set a tool's deadline in `tools/list` with `_meta["agent-runtime/timeoutMs"]`. * Each call's deadline is `timeoutMs` (default 60 seconds, at most 20 minutes), restarted by each progress notification. A call from `js_exec` also ends with its execution (60 seconds at most), so expose long tools directly. * Text reaches the model as it is. Images, audio, blobs and text resources over 64 KiB are saved to the agent's workspace and reach it as files. `isError` becomes a tool error; `structuredContent` is what code gets. * Saving the definition lists each server. A server that refuses its credentials is a 400, and the answer's `toolSources` shows what each server offers. A server that can't be reached when an agent starts contributes no tools; the run's `sourceErrors` lists it. * Tool lists are cached for five minutes and dropped on `notifications/tools/list_changed`. Progress a server reports (`notifications/progress`) reaches the agent's events as `tool_execution_update`. * Each call carries `_meta` with `agent-runtime/idempotencyKey`, stable across attempts, plus the call ids, actor and origin, and `camelrun/files` when files are sent. For a short tour of MCP on camelRun, see [building and hosting an MCP agent](https://camelai.com/run/mcp). ### Fetched credentials A bearer token in a definition lives as long as the definition does. The credentials worth using don't: a GitHub App installation token lasts an hour, and so do cloud STS tokens and most OAuth access tokens. With `auth: {"type": "exchange"}`, camelRun fetches the server's credential from an endpoint of yours when it needs one, and fetches another before that one expires. ```json {"name": "Triage", "mcpServers": [{ "name": "github", "url": "https://mcp.example.com/github", "auth": {"type": "exchange", "url": "https://app.example.com/camelrun/github-token"} }]} ``` camelRun posts to your endpoint with an identity token in `Authorization` and `{"server": {"name", "url"}, "tenant", "definition", "agent"}` as the body. Your endpoint answers `{"token": "ghs_...", "expiresAt": "2026-10-13T19:00:00Z"}` (or `"expiresIn": 3600`, in seconds), and camelRun calls the server with `Authorization: Bearer `, plus the entry's static `headers`. The token is the one a served tool gets, except that `aud` is your endpoint's URL (or `auth.audience`), it carries `exchange: { server, url }` naming the tool server, and it has no `act`, `origin`, `req` or `tcid`: a credential serves everyone who talks to the agent, not one turn. Your endpoint must check the signature, issuer and expiry, that `aud` is its own, that **`tenant` is yours**, and that `exchange` names a server you issue for. To refuse, answer a 4xx with `{"error": "why"}`: the first 200 characters reach the tool error. `verifyRuntimeToken` from `@camelai/run/server` does the token checks. * A credential is kept per agent, in memory only. It is never stored, logged, returned by an API or shown to the model. * It is fetched again five minutes before it expires, and kept a day at most. * When the server answers 401 or 403, camelRun fetches another and tries once more. * Your endpoint has 5 seconds to answer. Network errors, timeouts, 5xx, 408 and 429 are retried up to three times within 8 seconds; then the call fails with `source_unavailable`. * Saving the definition asks for a credential as the definition (its token's `agent` and `sub` are the definition's id). An endpoint that refuses fails the save with a 400; one that is down doesn't. It is for a definition's MCP servers. OpenAPI sources and an agent's own MCP servers don't take it yet. ## OpenAPI specs ```json {"name": "Support", "openApi": [{ "name": "shop", "spec": "https://api.example.com/openapi.json", "auth": {"type": "bearer", "token": "..."}, "allowTools": ["listOrders", "getOrder", "refundOrder"] }]} ``` Every operation of an OpenAPI 3 spec (JSON or YAML) becomes a tool named `__` (or `___`). Its input is the operation's path, query and header parameters by name, plus `body` (JSON, or form-encoded). The spec is fetched and checked when the definition is saved, and its operations (at most 1024) are stored with it, so an agent's tools never change under it; save the definition again to pick up spec changes. `spec` may also be the document itself. Requests go to `baseUrl` (default the spec's first server), with no redirects, `timeoutMs` (default 30 seconds, at most 20 minutes) and each call's stable key as an `Idempotency-Key` header. A non-2xx answer becomes a tool error. ## An agent's own MCP servers An agent made without a definition, and a [stateless run](/docs/camelrun/stateless-runs), can list MCP servers itself: ```ts const agent = await agents.upsert("support", { instructions: "…", mcpServers: [{ name: "kb", url: "https://mcp.example.com/mcp", auth: { type: "runtime" } }], }); ``` * They take no credentials: `auth` is `{"type": "runtime"}` or left out, and there are no `headers`. A server with a token, headers or a credential exchange is a 400 saying to use a definition, which seals credentials once for every agent made from it. * Everything else is as in a definition: `allowTools`, `denyTools`, `exposure`, `timeoutMs`, `audience`, `approval`, and the same outbound checks. They are not listed when saved; a server that can't be listed when the agent starts shows in the run's `sourceErrors`. * `PATCH /v1/agents/{id}/configuration` with `mcpServers` replaces the list, and `null` or `[]` removes it. An upsert sets the whole list, so an upsert without `mcpServers` leaves none. * Only your API key changes them, never the agent's own token. An agent made from a definition has its definition's servers and refuses its own (400). * Only MCP servers: OpenAPI specs stay in definitions. ## Built-ins Tools camelRun answers itself. Give them as `builtins` on `upsert` (or `PATCH /v1/agents/{id}/configuration`), or in a definition: `"builtins": ["web_fetch", "web_search", "x_search", "schedule", "ask_user", "delegate", "generate_image", "agents"]`. | Built-in | | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `web_fetch` | Reads a public page as text (20,000 characters by default, at most 100,000), or saves a PDF or image to the workspace. Pages that are only a JavaScript shell are rendered when a Firecrawl key is available | | `web_search` | Returns `{query, provider, results}`, trying Exa, Brave and Parallel in order (a definition can pin its own with `"webSearch": {"providers": ["brave"]}`). Your own provider key is used first; otherwise the platform's, charged per search | | `x_search` | Searches X through Grok and returns a summary with citations. See [X search](#x-search) | | `schedule` | Lets the agent manage its own wake-ups with `schedule`, `list_schedules` and `cancel_schedule` (100 per agent, at most a year ahead, repeats at least a minute apart). `schedule` takes `inSeconds`, `at` or `everySeconds`, or a `calendar` (local `time`, IANA `timeZone`, optional `daysOfWeek`). See [Schedules](/docs/camelrun/schedules) | | `ask_user` | Lets the model ask 1 to 4 multiple-choice questions; the run waits for the answer | | `delegate` | With `delegate: { agents }`, hands a task to a sub-agent and returns its answer. See [Multi-agent](/docs/camelrun/multi-agent) | | `generate_image` | Makes an image, or edits images from the agent's files, and saves it to the workspace. It uses your OpenAI key, else the platform's, charged per token on prepaid credit. See [Images](/docs/camelrun/images) | | `agents` | With the same settings as `delegate`, starts sub-agents in the background (`spawn_agent`, `wait_agent`, `list_agents`); each one's answer arrives as a notification | Saving a definition or an agent with `web_search`, `x_search` or `generate_image` when no key can serve it answers with `warnings` saying which key would set it up, and the console shows them. ### X search `x_search` (`{query, allowedHandles?, excludedHandles?, fromDate?, toDate?}`) searches X through Grok and returns a summary, source citations and usage. It works with any main model. Set an xAI key under **Models & keys** (`PUT /v1/providers/xai/key`). Where the platform offers its own xAI key and your account may use it, `x_search` can run on that instead. * `allowedHandles` and `excludedHandles` each take up to 20 handles, with or without `@`. Use one filter or the other. * Dates are `YYYY-MM-DD` in UTC: `fromDate` is inclusive and `toDate` exclusive. * Each call uses `grok-4.7`, with at most two model turns and 1,800 output tokens. From `js_exec`, set the execution's `timeoutMs` to `150000` to allow for the search deadline. * The result has `text`, `citations: [{url, title?}]`, `incomplete` and `usage: {inputTokens, cachedInputTokens, outputTokens, searchCalls, postsFetched, profilesFetched, costUsd}`. An incomplete answer is marked; a failed or empty answer is a tool error. camelRun doesn't retry a failed request. * Cost is what xAI reports: Grok tokens plus the posts and profiles fetched, not a flat price per search. Usage counts even when the answer is incomplete or empty, and appears under `xai/x_search`. On the platform's key it is charged to prepaid credit at that cost, with no added search fee. Treat posts as claims to corroborate, not verified facts. ### What tools cost Usage records model calls and the tools that cost money (X search, web search, images) per agent, and per [workspace](/docs/camelrun/workspaces) when the agent is in one. Where you meter end users, their credits pay for these tools too (see [End-user credits](/docs/camelrun/end-user-credits)). See [Observability](/docs/camelrun/observability) for reading usage. ## Tools from code Every agent has `js_exec`: the model writes JavaScript that calls any of its tools, in a sandbox that can reach nothing else. It can `await tools.shop__getOrder({ id })`, run calls in parallel with `Promise.all`, use `fs` over its files, find tools in a large catalog with `tools.search(query)`, and read a schema with `tools.describe(name)`. Code runs for 30 seconds by default and 60 at most, with 2 seconds of CPU, 128 MiB of memory and 256 tool calls. See [Limits](/docs/camelrun/limits). `agent.client.execute(code)` runs code yourself, outside the model's history, which is handy for testing tools. ### Tool search `tools.search(query)` (or `tools.search(query, { namespace, limit })`) returns the best matches as `{name, description, input}`, most relevant first (20 by default, at most 128). `tools.namespaces()` lists the sources with their tool counts. Only tool names enter the sandbox, so a catalog of thousands of tools costs a script nothing until it asks. Ranking is by keywords and, on hosted camelRun, also by meaning; searches ranked by meaning are billed at cost. ### Without code: `codeMode: false` An agent made with `codeMode: false` (Python `code_mode=False`, in a definition too) has no `js_exec`. The model calls every tool directly, whatever its `exposure`, and camelRun's part of the system prompt covers only the tools it has. With `fileTools: false` as well and no tools of any source, it has no tools at all (not even `present_file`), and the system prompt is your instructions plus a short note on who sent each message. For a classifier, a judge or an agent that answers in a line, a request goes from about 2,000 input tokens to a few hundred. ```ts const voice = await agents.upsert("voice-12", { instructions, codeMode: false, fileTools: false }); ``` Set it when the agent is made. Changed later by an upsert, the agent's tools change at once, while a conversation already under way keeps the text it began with. ## See an agent's tools `GET /v1/agents/{id}` returns `toolSources` (SDK: `agents.runtime.toolSources(agentId)`, Python `tool_sources`): every source in order of precedence (the channel's, the attached application's, file tools, then the definition's), its status and tools, and why the model doesn't get a tool when it doesn't. `?schemas=true` adds input schemas; `?refresh=true` lists every MCP server now, which is how to check one before the agent runs. The console shows the same on an agent's Configuration tab. ## Files in tool calls Tools of a definition's sources take and return files without their bytes passing through the model. The model names a file by its path, as `{"$file": "/workspace/report.pdf"}`, and camelRun sends it the way the tool's schema asks. A string parameter takes a file when it is marked as MCP's file inputs proposal (SEP-2631, a draft) marks one: ```json {"type": "object", "properties": { "attachment": {"type": "string", "format": "uri", "x-mcp-file": {"accept": ["application/pdf", "image/*"], "maxSize": 10485760, "transferModes": ["url", "inline"]}}, "site": {"type": "string", "format": "uri", "x-mcp-file": {}, "x-camelrun-directory": true} }} ``` * `accept` and `maxSize` limit what it takes; anything else is a tool error. `transferModes` is `url` (the default), `inline`, or both. `x-camelrun-directory: true` makes it take a directory. * Unmarked parameters take files too: a base64 field gets the content (up to 4 MiB), and a `format: uri` field, or one named like a URL, gets a URL. An OpenAPI multipart or binary body streams the file. The tool receives a URL, `https:///v1/files//`, that answers `GET` (with `Range`) with the file; a `data:` URI for `inline` files of at most 4 MiB; or, for a directory, the URL of a manifest listing each file's URL, size and sha-256, plus a tar.gz `archive` of them all (at most 1,000 files and 256 MiB). An MCP call also carries `_meta["camelrun/files"]`, each file by its argument's JSON pointer. * A URL lasts 5 minutes and is bound to one call. It serves the version the call named and answers 410 once the file changes. A directory is a snapshot made for the call. An expired or altered URL answers 403. * `verifyFileUrl(url, { runtime, tenant, agent })` from `@camelai/run/server` (Python `verify_file_url`) checks that a URL came from camelRun, for the agent you expect. * `fileArguments` is on by default for sources with `auth: {"type": "runtime"}` (your own servers, which check who is calling) and off for every other source, so a model talked into it can't send the agent's files to another party's server. Set `"fileArguments": "on"` on a third-party server you trust with them. * Files a tool returns (images, audio, blobs, long text resources, binary OpenAPI responses, `web_fetch` downloads) are saved to `/workspace/tool-outputs///`, and the model gets a reference to each. One call may save 64 MiB, and one run 256 MiB. ## Outbound calls Every request to a URL you or the model chose (MCP servers, OpenAPI specs and APIs, credential endpoints, `web_fetch`) must be `https://`, carry no credentials in the URL, and resolve to a public address. The check runs on every connection, so DNS rebinding can't slip past it. MCP servers and API calls get no redirects. Where redirects are followed (`web_fetch`, spec downloads) each hop is checked, and credentials never go to another origin. --- --- title: 'Human in the loop: agent approvals on camelRun' sidebarTitle: 'Human input' description: 'Human-in-the-loop approvals, questions and forms for camelRun agents: the run suspends, waits minutes or days with nothing billed, then resumes.' --- [View as Markdown](https://camelai.com/docs/camelrun/human-input.md) · [Docs index for coding agents](https://run.camelai.com/llms.txt) A run can stop and wait for a person: the model asks a question, a tool needs approval before it runs, or a tool asks for a form or a setup step. The run **suspends**. It ends with `status: "input_required"` and the `inputs` it waits on, and the agent sleeps (nothing is billed while it waits). Once the last input is answered, minutes or days later and from any process, the turn resumes. The model sees an ordinary tool call and its result, told who answered and how long it took. For a short tour, see [human-in-the-loop AI agents on camelRun](https://camelai.com/run). ```ts TypeScript let run = await agent.run("Delete the staging database", { user: "alice" }); while (run.status === "input_required") { const input = run.inputs[0]; console.log(input.kind, input.message); // "approval", "Allow db__drop to run?" run = await input.answer(true, { from: "alice" }); // resolves with the resumed run } console.log(run.text); ``` ```python Python run = await agent.run("Delete the staging database", user="alice") while run.status == "input_required": run = await run.inputs[0].answer(True, from_="alice") print(run.text) ``` ## Where inputs come from * **Approvals.** A tool with `needsApproval: true` (`@tool(needs_approval=True)`), or a function of its arguments that decides per call, is approved before each call. A definition's MCP and OpenAPI sources take an approval policy: `"approval": {"default": "never" | "always" | "destructive", "tools": {"delete_repo": "always"}}`. `destructive` means MCP tools annotated `destructiveHint`, and OpenAPI operations other than GET, HEAD and OPTIONS. OpenAPI also takes `"methods": ["POST", "DELETE"]`. The person sees the real call: the tool, its source and its arguments. An approved call runs with exactly those arguments and carries the approval (`context.identity.approval`), so a tool can require it. A declined call never runs. * **Questions.** The `ask_user` built-in (`"builtins": ["ask_user"]` on the agent or its definition) lets the model ask 1 to 4 multiple-choice questions, with free text where it allows. * **The tool itself.** `context.confirm(message)`, `context.ask(message, schema)` (a flat form) and `context.requireUrl(url, message)` (a step on an https page, such as connecting an account). ```ts const deleteApp = tool({ description: "Delete an app", input: schema.Object({ app: schema.String() }), execute: async ({ app }, context) => { if (!await context.confirm(`Delete ${app}? Its URL stops working.`)) return { cancelled: true }; return apps.delete(app, { idempotencyKey: context.idempotencyKey }); }, }); ``` > **Warning:** > > An ask ends the call. Once the person answers, camelRun calls the tool again > with the same arguments and the ask returns the answer, so **everything before > an ask runs again**. Ask first and act after, and key side effects by > `context.idempotencyKey`, which is the same on both calls. Code in `js_exec` can't wait for days. A call from code that would ask fails with "needs the user's input: call it directly", so tools that ask are always offered to the model directly. ## Answering `input.answer(value, { from })` (Python `answer(value, from_=)`) takes a plain value by the input's `kind`: | Kind | Value | | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `approval` | `true` to approve, `false` to decline | | `question` | The chosen option's label (or labels, or free text where allowed). For several questions, `{ "": answer }` | | `form` | The fields, as an object, checked against the form's schema. A confirmation (`context.confirm`, a form without fields) takes `true` or `false` | | `url` | `true` once the person has done what the page asks, else `false` | `input.decline()` declines any input. Answering resolves with the resumed run. When the run also waits on other inputs, it resolves with the run still `input_required`, listing those. Answering the same way twice is safe; a different answer to an input that already settled is a 409 that says how it settled. Inputs outlive your process, so you can answer later from anywhere: * `agent.pendingInputs()` (Python `pending_inputs()`) lists an agent's waiting inputs, each with `answer()`. * `agents.runtime.inbox("pending")` lists inputs across all your agents, for an approvals queue. * Over REST: `GET /v1/agents/{id}/inputs?state=pending`, then `POST /v1/agents/{id}/inputs/{inputId}` with `{"action": "accept" | "decline" | "cancel", "content": ..., "from": ...}`. A question's `content` is `{"answers": {"": "