Replit Agent builds the app that runs your agent, but the agent's own tool-calling loop is a backend you prompt it to write. This guide builds a helpdesk triage agent: it reads new tickets from Replit's built-in PostgreSQL, drafts a resolution with Claude, and only pings Slack once a teammate clicks Approve. Budget 30-40 minutes on Replit's Core plan or a free trial.
| Fact | Value |
|---|---|
| Tool | Replit |
| Difficulty | Beginner |
| Time required | 30-40 min |
| Compatibility | Replit Core plan or free trial, Replit Agent, Anthropic API key, browser only |
| Last updated | November 2026 |
What you'll build
You'll build a helpdesk triage agent for an internal support inbox. New tickets land as rows in Replit's built-in PostgreSQL. A backend route calls Claude to draft a suggested resolution and a priority for each new ticket, writing both back to the row with status still open_draft. A small review page lists drafted tickets with an Escalate to Slack button; only clicking it calls the Slack webhook, and only after that does the status change to escalated. Nothing reaches Slack on its own.
Replit Agent writes this for you from prompts in its chat, the same way it would write any other feature of the app. The difference between a toy demo and this guide is where you draw the line between the model's draft and the action that leaves the sandbox: the drafting route only ever touches your own database, and the one route that calls an external webhook requires a row that's already marked approved. You keep that boundary by being explicit about it in every prompt, not by trusting the model to infer it.
Choose Replit when you want the backend, the database, and the agent logic in one place you can deploy directly from the browser. If you'd rather not depend on a hosted app builder and want the tool-calling loop as a plain script you run yourself, the Claude Agent SDK guide builds the same approval pattern in Python.
Prerequisites
- A Replit account on Core or a free trial with Agent access
- An Anthropic API key
- A Slack incoming webhook URL (or any URL you can point a test request at)
- No prior coding experience required; you'll prompt Replit Agent for the code
Step-by-step guide
Prompt Replit Agent to scaffold the app and the tickets table
Start a new Repl and describe the whole shape in your first prompt: a tickets table with a status column, a backend that already has a place to add routes, and a simple admin page to list tickets. Replit Agent provisions the built-in PostgreSQL database automatically when your prompt mentions needing one, and shows you the generated schema before running it.
1Prompt to Replit Agent:2"Build a Node/Express app with Replit's built-in PostgreSQL. Create a tickets table (id, subject, body, priority, draft_resolution, status default 'open', created_at). Add an /admin page listing tickets with their status. Don't wire up any external calls yet."Expected result: A running app with a tickets table visible in the Database pane and an /admin page that lists rows once you add test data.
Add your Anthropic key to Secrets
Open the Secrets pane (the lock icon in the sidebar) and add ANTHROPIC_API_KEY. Secrets are encrypted and injected as environment variables; your backend reads it with process.env.ANTHROPIC_API_KEY. Forking this Repl later copies the secret names but not their values, so a fork never silently leaks your key.
1Secrets pane -> Add new secret2Key: ANTHROPIC_API_KEY3Value: sk-ant-...Expected result: ANTHROPIC_API_KEY is listed in the Secrets pane and readable from a quick console.log(typeof process.env.ANTHROPIC_API_KEY) check.
Prompt for the drafting route
Ask Replit Agent to add the route that actually calls Claude. Be specific that it only writes a draft back to the row; it should never call anything outside your own database. This keeps the route safe to run on every new ticket without a human watching it, since the worst case is a bad draft, not a bad outside action.
1Prompt to Replit Agent:2"Add a POST /tickets/:id/draft route. It reads the ticket, calls the Anthropic API with model claude-sonnet-5-5 asking for a suggested resolution and a priority (low/medium/high), and updates the row's draft_resolution, priority, and status to 'open_draft'. It must not call Slack or any other external API."Expected result: Calling the route on a test ticket fills in draft_resolution and priority and flips status to open_draft, visible on /admin.
Add the Escalate action behind a second route
Prompt for a second route, separate from the drafting one, that sends the ticket's draft to Slack and only then marks it escalated. Point /admin's Escalate button at this route. Two routes instead of one is the whole safety mechanism here: nothing in the drafting path can reach Slack, so an escalation only happens when a person clicks the button.
1Prompt to Replit Agent:2"Add a POST /tickets/:id/escalate route that posts the ticket's subject and draft_resolution to the Slack webhook URL stored in the SLACK_WEBHOOK_URL secret, then sets status to 'escalated'. Wire the /admin page's Escalate button to call this route."Expected result: Clicking Escalate on a drafted ticket posts a real message to your Slack channel and the row's status updates to escalated.
Test the loop, watch for cold starts, and fix the webhook gotcha
Run the drafting route on a few test tickets, escalate one, and confirm it lands in Slack. If you're testing against an Autoscale deployment rather than the dev Repl, expect the first request after idle time to feel slow; that's a cold start, not a bug. If Slack ever reports a broken signature or a redirect error, check whether Replit's own redirect layer (Replshield) is stripping headers on an http:// webhook URL; switch it to https:// and the problem goes away.
1Test sequence:2POST /tickets/1/draft -> status becomes open_draft3POST /tickets/1/escalate -> Slack message arrives, status becomes escalatedExpected result: A full run from draft to a real Slack message, with status changes visible on /admin after each step.
Deploy and add a rate limit before going live
Pick an Autoscale deployment if tickets arrive sporadically and you don't mind a cold start on the first request, or a Reserved VM if you need the draft route to respond instantly to a webhook from your ticketing system. Before deploying, prompt for a simple rate limit on /tickets/:id/draft so a bug that retries in a loop can't burn through your Anthropic credits in minutes.
1Prompt to Replit Agent:2"Add a rate limit to POST /tickets/:id/draft: no more than 1 draft call per ticket per 5 minutes, returning 429 if exceeded."Expected result: A second draft request on the same ticket within 5 minutes returns 429 instead of calling Claude again.
Complete working example
1// Generated by Replit Agent from the prompts in steps 3-4 and 6; shown here2// so you can see exactly what it wrote. Re-prompt to change it rather than3// hand-editing, so the app and this file stay in sync.4import express from "express";5import { pool } from "../db.js";67const router = express.Router();8const lastDraftAt = new Map();910router.post("/tickets/:id/draft", async (req, res) => {11 const { id } = req.params;12 const last = lastDraftAt.get(id);13 if (last && Date.now() - last < 5 * 60 * 1000) {14 return res.status(429).json({ error: "rate_limited" });15 }1617 const { rows } = await pool.query("SELECT * FROM tickets WHERE id = $1", [id]);18 const ticket = rows[0];19 if (!ticket) return res.status(404).json({ error: "not_found" });2021 const claudeRes = await fetch("https://api.anthropic.com/v1/messages", {22 method: "POST",23 headers: {24 "content-type": "application/json",25 "x-api-key": process.env.ANTHROPIC_API_KEY,26 "anthropic-version": "2023-06-01",27 },28 body: JSON.stringify({29 model: "claude-sonnet-5-5",30 max_tokens: 400,31 messages: [{32 role: "user",33 content: `Ticket: ${ticket.subject}\n${ticket.body}\n\nSuggest a resolution and a priority (low/medium/high). Return JSON: {"resolution": "...", "priority": "..."}.`,34 }],35 }),36 });37 const data = await claudeRes.json();38 const parsed = JSON.parse(data.content[0].text);3940 await pool.query(41 "UPDATE tickets SET draft_resolution = $1, priority = $2, status = 'open_draft' WHERE id = $3",42 [parsed.resolution, parsed.priority, id]43 );44 lastDraftAt.set(id, Date.now());45 res.json({ drafted: true });46});4748router.post("/tickets/:id/escalate", async (req, res) => {49 const { id } = req.params;50 const { rows } = await pool.query("SELECT * FROM tickets WHERE id = $1", [id]);51 const ticket = rows[0];52 if (!ticket) return res.status(404).json({ error: "not_found" });5354 await fetch(process.env.SLACK_WEBHOOK_URL, {55 method: "POST",56 headers: { "content-type": "application/json" },57 body: JSON.stringify({58 text: `Escalated: ${ticket.subject}\nSuggested resolution: ${ticket.draft_resolution}`,59 }),60 });6162 await pool.query("UPDATE tickets SET status = 'escalated' WHERE id = $1", [id]);63 res.json({ escalated: true });64});6566export default router;67Common mistakes
Why it's a problem: Letting one route both draft the resolution and post to Slack
How to avoid: Collapsing both actions into one route removes the boundary a human approval step needs. Keep /draft and /escalate as separate routes, like the complete_code above.
Why it's a problem: Hardcoding the Slack webhook URL or Anthropic key directly in a route file
How to avoid: Always add them through the Secrets pane and read them with process.env. A key committed to a file shows up in version history even if you delete it later.
Why it's a problem: Assuming OAuth callbacks or webhooks will work against your dev Repl's preview URL
How to avoid: Use the deployed *.replit.app URL for OAuth redirects and incoming webhooks. The dev preview URL changes and isn't meant for production traffic.
Why it's a problem: Expecting an Autoscale deployment to respond instantly to the first request after idle time
How to avoid: Autoscale spins down to zero and cold-starts on the next request, including dropping any pooled database connection. Use a Reserved VM if a webhook sender has a short timeout.
Why it's a problem: Skipping a rate limit on the drafting route because 'it's just internal'
How to avoid: A ticketing system retrying a failed webhook, or a bug in your own code, can call /draft far more often than a human would. Cap it before you deploy, not after the Anthropic bill arrives.
Best practices
- Keep the route that calls an outside API (Slack, email, a payments provider) separate from the route that only drafts into your own database.
- Store every credential in the Secrets pane; never let Replit Agent inline one into a generated file, even temporarily for testing.
- Pin a specific, current model id in the prompt you give Replit Agent rather than letting it pick one; model defaults change over time.
- Rate-limit any route that calls an LLM on an event you don't fully control, like an inbound webhook or a form submission.
- Use a Reserved VM instead of Autoscale for any route that must respond to a webhook within a few seconds.
- Re-prompt Replit Agent to change generated code instead of hand-editing it under time pressure, so the diff it shows you stays accurate.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I have a Replit app with a POST /tickets/:id/draft route that drafts a resolution with Claude and a POST /tickets/:id/escalate route gated behind an Approve button (code below). Design a third route that auto-closes a ticket if it's been in open_draft for more than 72 hours with no escalation. [paste complete_code]
In Replit Agent's chat, prompt: 'Add a daily summary route that counts how many tickets were drafted and escalated in the last 24 hours and posts that count to the same Slack webhook every morning at 9am.'
Frequently asked questions
Do I need to know JavaScript to build an AI agent with Replit?
No. Every route in this guide comes from a prompt to Replit Agent, shown in each step. Reading the generated code in complete_code helps you understand what's running, but you make changes by re-prompting, not by writing JavaScript yourself.
Where does the agent logic actually run on Replit?
Inside your app's own backend, deployed as an Autoscale or Reserved VM Deployment. There's no separate 'agent runtime' outside the app; the Express routes you prompt for are the agent.
Why does my Secrets value not show up in process.env after I add it?
Restart the Repl. Secrets are injected when the process starts, so a value added while the app is already running won't appear until the next restart.
Can this agent call other tools besides Slack, like Stripe or an internal database?
Yes. Replit has a native /stripe command that auto-provisions sandbox keys and webhooks if you want to add billing, and the same draft-then-approve pattern works for any external call: keep it in its own route, gated behind a status check.
What does this cost to run?
Replit Core runs $25/mo with Agent access and Autoscale deployments. On top of that, each draft call to Claude Sonnet 5.5 on a short ticket runs a fraction of a cent, so a few thousand tickets a month is a few dollars of model cost.
Is a static IP a problem for this agent?
Replit has no static outbound IP by default, so if Slack or another service needs IP allowlisting, you'll need a workaround like a fixed egress proxy; a plain incoming webhook from Slack to your deployed URL, as this guide uses, doesn't have that problem.
Can someone build this for me instead of prompting it myself?
Yes. RapidDev builds production agents on Replit, Lovable, and the Claude Agent SDK, including the approval routing and rate limits most tutorials skip. A free 30-minute consultation is the fastest way to scope your version.
Talk to an Expert
Our team has built 1,000+ apps. Get personalized help with your project.
Book a free consultation