Docs
Bots
A Telegram or Slack bot as an always-on service.
A bot is an agent that never finishes: it waits for messages and answers them. On Keel that is an agent in service mode: the platform keeps one run of it up, restarts it when it crashes, gives it a persistent workspace for state such as the last processed update, and bills the hours it is up (Billing). This page deploys a Telegram bot twice, first with long polling (no inbound traffic needed), then behind a public hostname with a webhook.
Before you start
npm install -g @keelhosting/cliandkeel login(The CLI).- A Telegram bot token from BotFather.
- An organization on a plan with the
servicesfeature (keel usageshows the plan; business and enterprise include it). The webhook variant also needs theingressfeature.
Five commands: a long polling bot
mkdir support-bot && cd support-bot
keel init --template bot --name "Support Bot"
keel secrets set TELEGRAM_BOT_TOKEN
keel deploy
keel service start support-bot
What each step does:
keel init --template botwriteskeel.yaml, aDockerfile,.dockerignore,bot.pyand an emptyrequirements.txt. The bot uses only the Python standard library: it callsgetUpdateswith a 30 second long poll, answers every text message withYou said: ..., keeps the update offset in/keel/workspace/offsetso a restart never replays a message, and backs off (1 s doubling to 30 s) while Telegram is unreachable. The manifest is:mode: service workspace: mountPath: /keel/workspace sizeGb: 1 egress: customHosts: - api.telegram.org secrets: - TELEGRAM_BOT_TOKENEgress is deny-all by default, so
api.telegram.orghas to be listed. The token is a secret, never anenvvalue: it is injected at start and never printed.keel secrets set TELEGRAM_BOT_TOKENreads the token from a hidden prompt and stores it in the organization's secret store.keel deploybuilds and pushes the image with Docker, creates the agent in service mode (POST /v1/agentswithmode: service) and records the revision. The revision is refused withservices_not_in_planon a plan without services and withworkspace_too_largewhensizeGbexceeds the plan'sworkspaceGbPerAgent.keel service start support-botbrings the service up.keel service status support-bot --followprints the state changes and the bot's stdout:service up run 2c1f... 08:00:00.412 stdout polling telegram from update 0
Send the bot a message in Telegram and watch the echo. Then try the failure path: keel service status support-bot shows Restarts, Last exit and Next restart after a crash, and keel logs support-bot reads the current run's stderr without knowing a run id.
Deploying again (keel deploy after editing bot.py) is a rolling restart: the platform starts a run on the new revision and ends the previous one; the workspace, and therefore the offset, is kept. keel service stop support-bot ends the run and stops the hourly billing; the workspace stays until the agent is archived.
The webhook variant: a public hostname
Long polling needs no inbound traffic, which is why it is the default. When Telegram (or any other system) has to call you, add ingress:
mkdir hook-bot && cd hook-bot
keel init --template webhook-bot --name "Hook Bot"
keel secrets set TELEGRAM_BOT_TOKEN
keel deploy
keel service start hook-bot
keel ingress hook-bot
The template differs in three places:
keel.yamlgainsingress: { port: 8080 }. Deploying it needs theingressplan feature (ingress_not_in_planotherwise).bot.pyruns aThreadingHTTPServeron port 8080 that answersGET /healthzand accepts Telegram updates onPOST /telegram. Only that path is served; everything else is 404.- On start the bot registers the webhook: it reads the public url the platform injects as
KEEL_INGRESS_URLand callssetWebhookwith<KEEL_INGRESS_URL>/telegram. Without the variable it logs a hint and keeps serving, so the same image also runs locally.
keel ingress hook-bot prints the stable hostname, https://hook-bot-<short org id>.svc.<domain>, the current alias and whether the service is connected (a hostname only answers while the service is up). The dashboard's Service panel shows the same hostname with a copy button.
Rotating the alias
The alias is a second, random label for the same service. If a webhook url leaks, keel ingress hook-bot --rotate-alias (or Rotate alias in the dashboard) replaces it; the old alias stops resolving immediately and the stable hostname is unchanged. Register the webhook again afterwards, which the template does on its next restart: keel service restart hook-bot.
Watching it from everywhere
| Surface | Where |
|---|---|
| CLI | keel services, keel service status <slug> --follow, keel logs <slug>, keel ingress <slug> |
| Dashboard | /services for every service with live state and uptime; the agent page's Service panel for start, stop, restart, the hostname, the workspace meter and the restart history; /usage for the service hours of the period |
| MCP | keel_list_services, keel_get_service, keel_get_run_logs {agent}, keel_start_service, keel_stop_service, keel_restart_service, keel_get_ingress |
| API | HTTP API, section Services and ingress |
Limits worth knowing
- A plan allows
maxServicesservices running at once (service_limit_reachedbeyond that); stopped services do not count. - Service runs have no deadline:
ceilingSecondsis0anddeadlineAtisnullonGET /v1/runs/{id}, and the run detail page showsnone, runs until the service stops. keel service stoppauses the agent andkeel service startresumes it; an archived agent cannot be started (agent_not_live), and an agent switches betweenrunandservicemode only while it has no active run and no running service (mode_change_blocked).- Without
TELEGRAM_BOT_TOKENboth templates logno token, idleand stay up instead of crash looping; set the secret andkeel service restartthe bot.