#How to add an AI agent to your Rails app
To add an AI agent to your Rails app, create the agent from a chat with the Vatio MCP server, then build it into the code with npx @vatio-ai/skills and /vatio-init (steps below). Your coding agent reads your routes, controllers and sign-in. The agent shows up as a chat widget rendered by your layout, and with the vatio-sdk gem it knows who is signed in: your app signs a short-lived token for the current user, Vatio verifies it, and the agent's private tools call your own controllers for that user's data. Your team supervises the conversations in the inbox, and nothing reaches your users until you publish.
#Add the agent in two steps
Step 1, from any chat. Add the remote MCP server https://vatio.ai/mcp as a connector in Claude, ChatGPT or Cursor, sign in with a Vatio account and ask for the agent. It is created in a preview you can try; nothing reaches your users until you publish (MCP).
Step 2, build it into the Rails app. In the project, run:
npx @vatio-ai/skills/vatio-initRun the first line in a terminal at the root of your Rails app, and the second in Claude Code (in Cursor or Codex, ask it to use the vatio-init skill). /vatio-init looks at what already exists, then writes the agent and pushes a preview. The first time it pushes, your browser opens to sign in with email, Google or GitHub. Then ask your coding agent to put the widget tag in your layout. You can say what you want after /vatio-init:
/vatio-init an agent for our signed-in customers: answer from our help pages and check their orders through our Rails appAfter launch, /vatio-improve is the daily routine (After launch).
#What vatio-init finds in a Rails project
vatio-init reads your app before it writes anything, then tells you what it found and what it recommends:
- What the agent could call:
config/routes.rband the controllers behind it, starting with the ones that already answer JSON. - How your users sign in: Devise,
has_secure_password, the Rails authentication generator, or Supabase. Whichever it is, your backend signs the token (Authentication); on Supabase Auth, an Edge Function can (Supabase apps). - What it could answer from: your README, a
docs/folder, help and policy pages. What the agent needs from them becomes knowledge entries.
Then it asks only what the code can't answer: what the agent may do, what it must never say or promise, and when a person takes over. The workspace usually goes in the repository of the backend the agent calls, here your Rails app, so vatio.yml, tools/ and identity.pub sit next to the code they describe.
#Put the agent in your app
Ask your coding agent to add this code; it is shown here so you can review it.
Your coding agent adds gem "vatio-sdk", "~> 0.6" to your Gemfile and configures it once:
Vatio::Identity.configure do |c| c.audience = "ridgeline" # your workspace slug c.private_key = ENV.fetch("VATIO_IDENTITY_PRIVATE_KEY") # the PEM, not a pathendThe widget goes in app/views/layouts/application.html.erb, before </body>:
<%= raw Vatio::Identity.widget_tag( workspace: "ridgeline", token: ENV.fetch("VATIO_PUBLISHABLE_TOKEN"), subject: current_user&.id, claims: { name: current_user&.name, email: current_user&.email }) %>current_user is Devise's name; use whatever returns your signed-in user. The vatpub_ key is a publishable key, made for page source: it selects one workspace and environment, and works only from the origins listed on it, plus Vatio's own pages and the app builders. Create one for production:
vatio keys create publishable --env live --label web --origin https://ridgeline.exampleOrigins are exact scheme://host[:port] values, and a refused one is named in the browser console. They live on the token, never in vatio.yml: change them with vatio keys origins PREFIX URL... or in Settings → API keys in the inbox. Never put a vat_ developer token in a page. Rails renders the tag per request, so it is safe by default; if a page cache, a CDN or caches_action stores the layout, render it without subject: and call window.VatioWidget.identify(token) from an uncached endpoint (Ruby gem).
#Let it act for signed-in users
subject: is what tells the agent who the user is. widget_tag signs an RS256 token with sub set to the user's id, aud to your workspace slug and exp an hour away, and renders it as data-visitor-token. A signed-out visitor gets no token and only the agent's public tools. The key pair comes from vatio auth --new-key: identity.pub stays in the workspace, named in vatio.yml, and identity.pem goes into your app as the VATIO_IDENTITY_PRIVATE_KEY environment variable, never into vatio secrets or git. Vatio holds only the public half, so it can check a token but never mint one.
A tool marked access: private forwards that token to your API as Authorization: Bearer. The controller behind it verifies it with the same gem:
class Api::OrdersController < ActionController::API include Vatio::Identity::Authentication before_action :authenticate_vatio! # 401 unless the token verifies
def index orders = Order.where(user_id: vatio_subject).order(created_at: :desc).limit(10) render json: { orders: orders.as_json(only: %i[number status total]) } endendIt answers GET /api/me/orders (get "me/orders", to: "orders#index" inside namespace :api in your routes). authenticate_vatio! checks the signature with the algorithm pinned, never read from the token's header, requires aud to be your slug and exp to be in the future, and answers 401 otherwise: the same check Vatio runs. Inheriting from ActionController::API keeps your session login and CSRF check out of it, so a POST tool works too.
The user comes from the token, never from a parameter. A private tool cannot declare a user id (user_id, customer_id, account_id and similar) as a parameter, and the deploy fails if it does. Scope every query by vatio_subject, and by nothing the request body says (Private tools).
#What your coding agent writes
You don't need to edit these files by hand, but they live in your repository, so you can review them like the rest of your code. An excerpt of vatio.yml:
workspace: ridgelineauth: public_key: identity.pubagent: name: Ridgeline instructions: | You help signed-in customers of Ridgeline Outfitters with their orders and answer product, shipping and returns questions from knowledge. Orders: call my_orders and only state what it returned; if it could not confirm who the user is, ask them to sign in. Never promise a refund. tools: [my_orders, knowledge_lookup, ask_for_a_person] knowledge: [help]tools/my_orders.yml:
description: The signed-in customer's recent orders and their status.when_to_use: The customer asks about their own orders, a delivery or a return.access: privaterequest: method: GET base_url: "$env.RIDGELINE_API" path: /api/me/orders headers: Authorization: "Bearer $auth.token"respond: data: orders: "$.orders"There is no customer id in it: your controller reads the user from the token. RIDGELINE_API, your app's address, is a secret your coding agent stores in Vatio with vatio secrets set, never in the repository. Tools covers tools that act, errors and statuses.
tools/ask_for_a_person.yml, which the agent calls when a person should take over:
description: Tell the team that this conversation needs a person.when_to_use: > The customer asks for a refund or disputes a charge, you cannot resolve it with what you know and your other tools, or they ask to talk to a person.parameters: type: object properties: reason: type: string description: Why it needs a person, in one line, for whoever picks it up. required: [reason]access: publicrequest: method: POST base_url: "$env.RIDGELINE_API" path: /api/vatio/ask-for-a-person requires: [chat.url] body: reason: chat_url: "$chat.url" name: "$contact.name" phone: "$contact.phone_number"respond: message: > The team was told. Tell the customer plainly that you are checking this with a colleague, who will answer here.The controller behind it alerts whoever handles refunds, with a mailer, a Slack message or a WhatsApp that carries chat_url, the link that opens the conversation in the inbox. Their reply there takes it over, and the agent stops answering on that chat; until then it keeps helping. See Hand off to a person.
#Try it, then publish
vatio-init's push ends with a link to the inbox, with Try the agent open, where you try the preview on WhatsApp, Instagram or a web chat. The web chat is a signed-out visitor, so it only reaches public tools. To try what a signed-in user gets, give your development app preview's widget:
vatio keys create publishable --env preview --label dev --origin http://localhost:3000Set that key as VATIO_PUBLISHABLE_TOKEN in development and sign in as a test user. Tools run on Vatio's servers and can't reach localhost, so point preview at a tunnel to your app with vatio secrets set RIDGELINE_API https://YOUR-TUNNEL --env preview; live keeps calling production. Then ask about your own orders and check them against your app, ask for someone else's by name or email (it should find nothing), and ask for a refund (it should call ask_for_a_person and say it is checking with a colleague; your reply from the inbox takes the conversation over). Each reply in the inbox shows the tool calls behind it, with what your controller answered. When the agent does what you want, ask your coding agent to publish. Nothing reaches your users until you do.
#After launch
Your team reads real conversations in the inbox. On a wrong reply, Report this reply asks what should have happened, and that note becomes a test. Once a day, run /vatio-improve in your project: your coding agent fixes the cause where it lives (the instructions, a knowledge entry, a tool or your controller) and pushes it to preview, and you review the results and approve the update (Improve). The same agent answers on WhatsApp and Instagram (Pricing). A chat there has no page to carry a token, so the agent needs a sign_in_url, and with vatio-sdk that page is one redirect to Vatio::Identity.sign_in_redirect_url (Sign in from WhatsApp and Instagram).
#Frequently asked questions
#How do I add an AI agent to my Rails app?
Step 1: add the MCP server https://vatio.ai/mcp as a connector in Claude, ChatGPT or Cursor, sign in and ask for the agent; it is created in a preview. Step 2: in your Rails project run npx @vatio-ai/skills, then /vatio-init in Claude Code (in Cursor or Codex, ask it to use that skill). Your coding agent reads your routes, controllers and sign-in, and the widget goes in your layout with one helper from the vatio-sdk gem.
#How does the agent know which user is signed in to my Rails app?
The vatio-sdk gem's widget_tag signs a short-lived RS256 token for the signed-in user, with the private key from vatio auth --new-key, and renders it on the widget's script tag. Vatio verifies it with your public key, the only half it holds, so it can check who someone is but never create a token. A signed-out visitor gets no token and only the agent's public tools.
#How does a Rails controller check a request from a Vatio tool?
Include Vatio::Identity::Authentication and add before_action :authenticate_vatio!. It verifies the forwarded token with the algorithm pinned, requires an aud equal to your workspace slug and an exp that has not passed, and answers 401 otherwise. Then scope every query to vatio_subject, the user id from the token's sub.
#Related guides
- Build an AI agent for your app: The two-step path with your coding agent, from the first chat to publishing.
- An AI chat for your web app or site: One script tag puts the agent in your web app or site, themed to your brand and able to recognize signed-in users.
- Support for signed-in users: Answer from your docs, check the user's own account, troubleshoot and open a ticket with what was tried.