Building AI Agent Approval Workflows: Emoji Shortcuts, Telegram Bots & Mobile-First Permissions
Building AI Agent Approval Workflows: Emoji Shortcuts, Telegram Bots & Mobile-First Permissions A practical guide to the part of agent design nobody demos: the moment your autonomous agent stops and asks "can I run this?" We cover three...
Primary Focus
ai agentsAI Tools Covered
What You'll Learn
- ✓The "5.24" That Isn't a Version Number
- ✓The Approval Features That Are Real
- ✓The Claims You Should Treat as Unverified
- ✓An Approval Is a Pending Promise
- ✓Resolve a Decision From One Emoji
- ✓Why Emoji Is a Shortcut, Not a Gate
Guide Curriculum
What Actually Ships vs. What the Demo Promises
Learn key concepts
- •The "5.24" That Isn't a Version Number1m
- •The Approval Features That Are Real1m
- •The Claims You Should Treat as Unverified1m
Emoji Shortcuts — Reaction-Based Approvals
Learn key concepts
- •An Approval Is a Pending Promise2m
- •Resolve a Decision From One Emoji2m
- •Why Emoji Is a Shortcut, Not a Gate1m
Telegram Bot Integration — Inline-Keyboard Approval Buttons
Learn key concepts
- •The Telegram Primitives You Need1m
- •A Complete Approval Bot2m
- •Lock Down Who Can Approve1m
Mobile-First Permission UX — Risk Tiers, Async Auth, Fail-Closed
Learn key concepts
- •Not Every Action Deserves the Same Friction2m
- •Async, Fail-Closed Gate2m
- •The Audit Trail Is the Product1m
Preview: First Lesson
What Actually Ships vs. What the Demo Promises
The "5.24" That Isn't a Version Number
Objectives
- Separate verified OpenClaw approval features from the video's aspirational claims.
- Understand why every claim in an "update just dropped" video should be read as should, not does.
- Establish the vocabulary used across the rest of this guide.
The source video is titled "OpenClaw 5.24 Update Just Dropped" and walks through a pre-release beta on GitHub. Before you copy any command from a video like this, check the primary source. OpenClaw does not use a 5.24 version scheme. The project switched to date-based release trains — YYYY.M.PATCH — and the current beta line as of this writing is v2026.6.5-beta.5, published June 8, 2026. The release notes literally say: "switch release trains to YYYY.M.PATCH monthly patch numbering."
So "5.24" is the video creator's own shorthand, not a tag you will find on the OpenClaw releases page. This matters because the rest of the video inherits the same loose relationship with the source. The narrator is admirably honest about it — he repeats the word "should" on nearly every feature and says outright: "I always apply should instead of does." That is your cue to verify before you build.
Start learning with this comprehensive guide
This guide includes:
About the Author
Hiram Clark is the founder and managing editor of vybecoding.ai and sets editorial direction for the guides and news published here. Articles are drafted with AI assistance and edited before publication. He works hands-on with the AI development tools, workflows, and infrastructure covered on the site.
Full Guide Content
Complete lesson text — start the interactive course above for exercises and progress tracking.
Module 1What Actually Ships vs. What the Demo Promises
1.1The "5.24" That Isn't a Version Number
- Separate verified OpenClaw approval features from the video's aspirational claims.
- Understand why every claim in an "update just dropped" video should be read as should, not does.
- Establish the vocabulary used across the rest of this guide.
The source video is titled "OpenClaw 5.24 Update Just Dropped" and walks through a pre-release beta on GitHub. Before you copy any command from a video like this, check the primary source. OpenClaw does not use a 5.24 version scheme. The project switched to date-based release trains — YYYY.M.PATCH — and the current beta line as of this writing is v2026.6.5-beta.5, published June 8, 2026. The release notes literally say: "switch release trains to YYYY.M.PATCH monthly patch numbering."
So "5.24" is the video creator's own shorthand, not a tag you will find on the OpenClaw releases page. This matters because the rest of the video inherits the same loose relationship with the source. The narrator is admirably honest about it — he repeats the word "should" on nearly every feature and says outright: "I always apply should instead of does." That is your cue to verify before you build.
1.2The Approval Features That Are Real
Here is what the OpenClaw beta release notes actually document about approvals — verified against the GitHub releases, not the video:
- Telegram exec approval allowlists. Release
v2026.6.2-beta.1notes: "keep Telegram DM exec approval allowlists working withask:off." This is a real, shipped approval mechanism: OpenClaw can require human sign-off before executing a command, gated per-channel, andask:offdisables the prompt for allowlisted callers. - Admin-gated writeback. The same release: "require admin rights for Telegram target writeback." Approvals are tied to channel permissions, not just to whoever sends a message.
- Native approval cards on Google Chat. The
v2026.6.5line: "add native approval card actions and click handling so Google Chat approvals use platform-native cards instead of generic message flow." Platform-native approval UI is the direction the project is moving.
1.3The Claims You Should Treat as Unverified
These appear in the video but are not in the verified release notes, so treat them as roadmap, not reality:
- "Approve from iMessage with a single thumbs-up emoji." The video says WhatsApp already had it and iMessage is catching up. The release notes for the beta line do not document an iMessage thumbs-up flow. It may exist behind a flag or land later — but do not architect around it until you confirm it.
- Meeting-notes plugin, real-time voice steering, and wake-name gating. Interesting, but unrelated to approvals and unverified here.
The good news: the pattern the video is selling — approve sensitive agent actions with a single tap from whatever app you already have open — is sound, well-understood, and buildable today on primitives that are fully documented. The next three modules build exactly that, independent of any one vendor's beta.
Module 2Emoji Shortcuts — Reaction-Based Approvals
2.1An Approval Is a Pending Promise
- Model an approval as a pending decision keyed to a message.
- Resolve that decision from a single emoji reaction.
- Understand why an emoji reaction is a UX shortcut, not a security boundary.
Strip away the messaging layer and an approval is just a deferred decision. Your agent reaches a gated action, creates a pending record, sends a notification, and waits — it does not block a thread; it parks the action and resolves it when a human answers. Every channel (emoji, button, push) is just a different front-end onto the same record.
The minimal shape of that record:
// lib/approvals/types.ts
export interface PendingApproval {
id: string; // stable id, also embedded in the notification
action: string; // e.g. "shell.exec", "email.send", "db.delete"
payload: Record;
channel: "emoji" | "telegram" | "push";
requestedAt: number; // epoch ms
expiresAt: number; // fail-closed deadline
status: "pending" | "approved" | "denied" | "expired";
decidedBy?: string; // who answered
decidedAt?: number;
}
The two fields people forget are expiresAt and decidedBy. The first makes the system fail closed — an unanswered request denies itself rather than hanging forever. The second gives you an audit trail, which Module 4 treats as non-negotiable.
2.2Resolve a Decision From One Emoji
A reaction-based approval maps a specific emoji to approve and another to deny. OpenClaw's own Telegram exec-approval allowlist is the production version of this idea; the durable, vendor-neutral version is a webhook that receives a reaction event and resolves the matching pending approval.
This handler is framework-agnostic — point it at any chat platform's reaction webhook (Slack reaction_added, a Telegram reaction update, an iMessage bridge):
// lib/approvals/emojiHandler.ts
import { logger } from "@/lib/logger";
import { resolveApproval, findApprovalByMessageRef } from "./store";
// Map reaction emoji -> decision. Keep this tiny and explicit.
const DECISION_BY_EMOJI: Record = {
"👍": "approved",
"✅": "approved",
"👎": "denied",
"❌": "denied",
};
interface ReactionEvent {
messageRef: string; // the notification message the reaction was added to
emoji: string;
userId: string;
}
export async function handleReaction(event: ReactionEvent): Promise {
const decision = DECISION_BY_EMOJI[event.emoji];
if (!decision) return; // ignore unrelated reactions silently
const approval = await findApprovalByMessageRef(event.messageRef);
if (!approval) {
logger.warn("Reaction on unknown approval message", { ref: event.messageRef });
return;
}
if (approval.status !== "pending") {
logger.info("Reaction on already-decided approval", { id: approval.id });
return; // idempotent: first reaction wins, later ones are no-ops
}
if (Date.now() > approval.expiresAt) {
await resolveApproval(approval.id, "expired", event.userId);
return; // fail closed
}
await resolveApproval(approval.id, decision, event.userId);
logger.info("Approval resolved via emoji", {
id: approval.id,
decision,
by: event.userId,
});
} 2.3Why Emoji Is a Shortcut, Not a Gate
An emoji reaction is fast, but it inherits the trust model of the channel it rides on. Anyone who can react to the message can decide the action. That is fine for low-risk actions ("post the draft tweet") and dangerous for high-risk ones ("wire the refund"). The fix is not to abandon emoji — it is to tier the actions, which is Module 4. Treat the emoji handler above as the convenient path for low-risk approvals and require a stronger channel for anything financially or destructively material.
Module 3Telegram Bot Integration — Inline-Keyboard Approval Buttons
3.1The Telegram Primitives You Need
- Send an approval request as a Telegram message with Approve/Deny buttons.
- Receive and answer the resulting callback query correctly.
- Edit the message after a decision so it can't be double-clicked.
Telegram's inline keyboards are the cleanest mobile approval surface that does not require building an app. The relevant primitives, quoted from the Telegram Bot API:
InlineKeyboardButton— a button with atextlabel and acallback_datastring (the payload sent back when pressed). Pressing a callback button "doesn't send messages to the chat"; it delivers data straight to your bot.CallbackQuery— the update your bot receives when a button is pressed. Key fields:id(String),from(User),message(Message, optional),data(String, optional — yourcallback_data), andchat_instance(String).answerCallbackQuery— the method you must call to dismiss the button's spinner. Parameters:callback_query_id(String, required),text(String, optional notification),show_alert(Boolean, optional).
The non-obvious rule: Telegram shows a loading spinner on the pressed button until you call answerCallbackQuery. Forget it and the UI looks frozen even though your handler ran.
3.2A Complete Approval Bot
This is a runnable Node.js bot (no framework, just fetch against the Bot API). It sends an approval request and resolves it from the button press. Set TELEGRAM_BOT_TOKEN and TELEGRAM_CHAT_ID in your environment.
// scripts/telegram-approval-bot.ts
import { logger } from "@/lib/logger";
const TOKEN = process.env.TELEGRAM_BOT_TOKEN!;
const API = `https://api.telegram.org/bot${TOKEN}`;
async function tg(method: string, body: Record) {
const res = await fetch(`${API}/${method}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${method} failed: ${res.status}`);
return res.json();
}
// 1. Send an approval request with two callback buttons.
export async function requestApproval(
chatId: string,
approvalId: string,
summary: string,
) {
return tg("sendMessage", {
chat_id: chatId,
text: `🔐 *Approval needed*\n${summary}`,
parse_mode: "Markdown",
reply_markup: {
inline_keyboard: [
[
{ text: "✅ Approve", callback_data: `approve:${approvalId}` },
{ text: "❌ Deny", callback_data: `deny:${approvalId}` },
],
],
},
});
}
// 2. Handle the callback when the user taps a button.
// Named types beat inline anonymous shapes — reuse and readability.
interface TelegramCallbackUpdate {
callback_query?: {
id: string;
from: { id: number };
data?: string;
message?: { chat: { id: number }; message_id: number };
};
}
// Only these Telegram user IDs may decide (Lesson 9 allowlist, inlined).
const APPROVER_IDS = new Set([
Number(process.env.TELEGRAM_APPROVER_ID),
]);
export async function handleCallback(update: TelegramCallbackUpdate) {
const cb = update.callback_query;
if (!cb?.data) return;
// Reject anyone not on the allowlist BEFORE acting on their tap.
if (!APPROVER_IDS.has(cb.from.id)) {
await tg("answerCallbackQuery", {
callback_query_id: cb.id,
text: "Not authorized to approve.",
show_alert: true,
});
return;
}
const [decision, approvalId] = cb.data.split(":");
const approved = decision === "approve";
// Resolve your pending approval here (see Module 2 store).
logger.info("Telegram approval decision", { approvalId, approved });
// MUST answer to clear the button spinner.
await tg("answerCallbackQuery", {
callback_query_id: cb.id,
text: approved ? "Approved ✅" : "Denied ❌",
});
// Edit the message so the buttons can't be pressed twice.
if (cb.message) {
await tg("editMessageText", {
chat_id: cb.message.chat.id,
message_id: cb.message.message_id,
text: `${approved ? "✅ Approved" : "❌ Denied"} — decision recorded.`,
});
}
} 3.3Lock Down Who Can Approve
A bot token is a bearer credential — anyone who can message the bot can press its buttons unless you gate it. Two defenses, both of which OpenClaw's release notes echo ("require admin rights for Telegram target writeback," "exec approval allowlists"):
- Allowlist the approver. Check
callback_query.from.idagainst a known set of admin user IDs before honoring the decision. Reject everyone else and answer withshow_alert: trueso they see it was refused. - Sign the
callback_data.callback_datais capped at 64 bytes and is attacker-visible. Don't trust it as the source of truth — look the approval up by id in your own store and verify it is stillpendingbefore acting. Theapprove:string is a pointer, not a permission.
Module 4Mobile-First Permission UX — Risk Tiers, Async Auth, Fail-Closed
4.1Not Every Action Deserves the Same Friction
- Tier actions by risk so trivial approvals stay one-tap and dangerous ones get friction.
- Make approvals asynchronous and fail-closed.
- Log every decision for audit.
The research consensus on mobile-first agent approvals (Auth0, Arahi, and others) is consistent: a smartphone push you can approve in one tap, with a round-trip under two seconds, is the ideal — for the right actions. The discipline is tiering. A common, well-documented pattern gates approval per agent, per tool, and per data scope, with the bar rising for "externally visible or financially material actions" — outbound emails, refunds or charges above a threshold, public posts, destructive database writes.
Encode the tiers as config, not as scattered if statements:
# config/approval-policy.yaml
# Tier determines BOTH whether approval is needed and how strong it must be.
tiers:
auto: # no human needed
actions: ["read.*", "search.*", "draft.create"]
one_tap: # emoji or single button is fine (Modules 2-3)
actions: ["email.send.internal", "calendar.create", "tweet.post"]
channels: ["emoji", "telegram"]
expires_in_seconds: 900
step_up: # requires a second factor before the tap counts
actions: ["payment.refund", "db.delete", "email.send.external"]
channels: ["push"]
require_reauth: true
expires_in_seconds: 300
default_tier: step_up # unknown actions get the STRONGEST gate, not the weakest
The single most important line is default_tier: step_up. An action your policy has never seen must fall into the most restrictive tier, never the least. New tools are added all the time; a permissive default is how an agent quietly gains the ability to do something dangerous without anyone approving it.
4.2Async, Fail-Closed Gate
Asynchronous authorization is the security property that makes all of this safe: even if the agent is compromised, it cannot execute a gated action without an explicit human decision, and an unanswered request expires into a denial. Here is the gate that ties Modules 2–4 together:
// lib/approvals/gate.ts
import { logger } from "@/lib/logger";
import { createPending, waitForDecision } from "./store";
import { tierFor } from "./policy";
interface GateResult {
allowed: boolean;
reason: "approved" | "denied" | "expired" | "auto";
decidedBy?: string;
}
export async function approvalGate(
action: string,
payload: Record,
): Promise {
const tier = tierFor(action); // reads approval-policy.yaml
if (tier.name === "auto") {
return { allowed: true, reason: "auto" };
}
const approval = await createPending({
action,
payload,
expiresAt: Date.now() + tier.expiresInSeconds * 1000,
requireReauth: tier.requireReauth ?? false,
});
// Notify on the tier's channels (emoji / telegram / push) — fire and forget.
// ⚠️ notifyApprovers is the ONE stub you must implement before this gate is
// production-usable: wire it to Module 2's emoji handler and Module 3's bot.
await notifyApprovers(approval, tier.channels);
// Block the ACTION (not the process) until decided or expired.
const decision = await waitForDecision(approval.id, approval.expiresAt);
// Audit EVERY outcome, including expiry. Non-negotiable.
logger.info("approval.decided", {
id: approval.id,
action,
outcome: decision.status,
decidedBy: decision.decidedBy,
});
return {
allowed: decision.status === "approved",
reason: decision.status,
decidedBy: decision.decidedBy,
};
}
// Stub — THIS IS YOUR INTEGRATION POINT. Replace the body with real dispatch:
// send the Telegram approval message (Module 3 `requestApproval`) and/or post
// the notification your emoji handler (Module 2) reacts to. The gate is not
// production-ready until this function actually reaches a human.
async function notifyApprovers(approval: { id: string }, channels: string[]) {
logger.debug("notifying approvers", { id: approval.id, channels });
} 4.3The Audit Trail Is the Product
Every source on production human-in-the-loop systems lands on the same requirement: log every request and every decision, so you can later answer "why did the agent do that?" Notice the logger.info("approval.decided", ...) call fires on all outcomes — approved, denied, and expired — with who decided and what action it was. That log is not a debugging nicety; it is the evidence that your agent only acted with permission. If a decision isn't logged, for compliance purposes it didn't happen. Build the audit trail first and the approval UI second, never the reverse.
References
- OpenClaw 5.24 Update Just Dropped… (source video)
- OpenClaw releases — GitHub (verified versioning + approval release notes)
- OpenClaw
- Telegram Bot API reference (
InlineKeyboardButton,CallbackQuery,answerCallbackQuery) - Telegram Bot features — inline keyboards & callback buttons
- Auth0 — Secure "Human in the Loop" Interactions for AI Agents
- Arahi AI — Human Approval for AI Agents