Skip to main content

Production Checklist

Before you publish, run through this list. Each item is what separates a demo from a bot people actually rely on.

Handle empty states

Every list view should say something useful when there's nothing to show.

when util.len(user.tasks) == 0 {
send.text("📭 No tasks yet — add one with /add!")
} else {
foreach t in user.tasks { send.text(`• ${t.title}`) }
}

Handle ask timeout and cancel

Every blocking ask.* should route "timeout" (and ideally "error"):

branch ask.text("Your answer?", timeout: "120s") {
"timeout" { send.text("No response — say /start to retry.") stop }
default { /* use answer */ }
}

Provide /help and navigation

  • Every bot has a /help flow explaining its commands.
  • Multi-step menus offer Back and Home buttons so users never get stuck.
flow help on command "/help" {
send.text("/start — menu\n/play — start a round\n/help — this message")
}

Keep secrets in global.*

API keys, IPN secrets, admin ids — all in global.*, set in the Variables panel. Never hardcoded.

global api_key: String = ""
global admin_id: Number = 0

Retry external calls

Wrap mission-critical HTTP in on error, or use the http.retry recipe for flaky upstreams.

res = http.get(url: flow.url) on error { send.text("Service unavailable.") stop }

Paginate long lists

Anything over ~10 items should page. Use the flow.paginate recipe or manual prefix-callback paging — see Pagination & limits.

Guard admin actions

Restrict destructive or privileged commands with guard.adminOnly() (or an explicit when user.id != global.admin_id check).

flow reset on command "/reset" {
guard.adminOnly() {
body { set { shared.scores = [] } send.text("Reset done.") }
onDeny { send.text("🚫 Admins only.") }
}
}

Rate-limit abusable actions

Throttle claims, submissions, and anything spammy with flow.rateLimit or a cooldown ledger. See Security & validation.

Make webhooks idempotent

Payment and callback webhooks can fire more than once. Deduplicate before crediting:

set {
shared.paid = array.reject(shared.paid, "uid", flow.uid)
shared.paid << [{ uid: flow.uid }]
}

And always verify, then re-check against the source API before delivering goods. See Payments & webhooks.

Use the right scope

  • user.* for per-user state.
  • shared.* for anything cross-user (and anything a schedule/webhook must read).
  • flow.* only for scratch within a single run — never long-lived state.

Final pass

CheckDone?
Empty states handled
ask.* timeouts handled
/help flow exists
Secrets in global.* only
HTTP calls have on error / retry
Long lists paginate
Admin commands guarded
Webhooks verified + idempotent
Every callback button has a handler
No editor errors before publish

Next