xiaohongshu
xiaohongshu
hitl-approval
Implement human-in-the-loop approval flows. Use when adding Slack approvals, dashboard review queues, action gating, or trust-based auto-approval.
Full skill instructions
Core differentiator: agent proposes, human approves. Every sensitive action requires human approval until trust is established.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Agent Tool │ ──▶ │ requestApproval │ ──▶ │ Slack Message │
│ proposes │ │ workflow │ │ with buttons │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ Action │ ◀── │ executeApproved │ ◀── │ Interactions │
│ executed │ │ Action workflow │ │ handler │
└─────────────────┘ └──────────────────┘ └─────────────────┘
| Surface | Use Case | Capabilities |
|---|---|---|
| Slack | Real-time push notifications | Approve/reject buttons, rejection reason modal |
| Dashboard | Bulk review, audit trail | Queue view, search, bulk actions |
| Front Plugin | In-context approval | View customer data, quick actions |
| Event | Trigger | Data |
|---|---|---|
support/approval.requested | Agent proposes action | actionId, conversationId, appId, action, agentReasoning |
support/approval.decided | Human clicks button | actionId, decision, decidedBy, decidedAt |
support/action.approved | Human approves | actionId, approvedBy, approvedAt |
support/action.rejected | Human rejects | actionId, rejectedBy, rejectedAt, reason? |
IMPORTANT: The waitForEvent match uses data.actionId - ensure all events use actionId consistently (not approvalId).
Build approval messages with buildApprovalBlocks:
import { buildApprovalBlocks } from '@skillrecordings/core/slack/approval-blocks'
const blocks = buildApprovalBlocks({
actionId: 'action-123',
conversationId: 'conv-456',
appId: 'total-typescript',
actionType: 'issue_refund',
parameters: { orderId: 'order-789', amount: 99.00 },
agentReasoning: 'Customer within 30-day refund window',
})
// Returns: header, reasoning section, parameters, approve/reject buttons
Button metadata structure (embedded in value):
{ actionId, conversationId, appId }
import { postApprovalMessage, updateApprovalMessage } from '@skillrecordings/core/slack/client'
// Post approval message to channel
const { ts, channel } = await postApprovalMessage(
process.env.SLACK_APPROVAL_CHANNEL,
blocks,
'Approval needed for Issue Refund'
)
// Update message after decision
await updateApprovalMessage(channel, ts, updatedBlocks, 'Approved')
// apps/slack/app/api/slack/interactions/route.ts
import { verifySlackSignature } from '../../../../lib/verify-signature'
import { inngest, SUPPORT_APPROVAL_DECIDED, SUPPORT_ACTION_APPROVED } from '@skillrecordings/core/inngest'
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('x-slack-signature') ?? ''
const timestamp = request.headers.get('x-slack-request-timestamp') ?? ''
// Verify signature (HMAC-SHA256, 5-min replay protection)
if (!verifySlackSignature({ signature, timestamp, body })) {
return new Response('Invalid signature', { status: 401 })
}
// Parse URL-encoded payload
const params = new URLSearchParams(body)
const payload = JSON.parse(params.get('payload') ?? '{}')
if (payload.type === 'block_actions') {
const action = payload.actions[0]
const metadata = JSON.parse(action.value)
if (action.action_id === 'approve_action') {
await inngest.send([
{ name: SUPPORT_APPROVAL_DECIDED, data: { approvalId: metadata.actionId, decision: 'approved', ... } },
{ name: SUPPORT_ACTION_APPROVED, data: { actionId: metadata.actionId, ... } },
])
}
}
return new Response('OK', { status: 200 }) // Slack requires quick 200
}
import { verifySlackSignature } from 'apps/slack/lib/verify-signature'
// Returns true if valid, false if invalid/expired
// Throws if SLACK_SIGNING_SECRET missing
const isValid = verifySlackSignature({
signature: request.headers.get('x-slack-signature'),
timestamp: request.headers.get('x-slack-request-timestamp'),
body: rawRequestBody,
// secret defaults to process.env.SLACK_SIGNING_SECRET
})
Key features:
// AUTO-APPROVE (execute immediately)
- Magic link requests
- Password reset requests
- Refunds within 30 days of purchase
- Transfers within 14 days of purchase
- Email/name updates
// REQUIRE-APPROVAL (draft action, wait for human)
- Refunds 30-45 days after purchase
- Transfers after 14 days
- Bulk seat management
- Account deletions
// ALWAYS-ESCALATE (flag for human, do not act)
- Angry/frustrated customers (sentiment detection)
- Legal language (lawsuit, lawyer, etc.)
- Repeated failed interactions
- Anything uncertain
Track how much humans modify agent drafts to build trust:
function diffScore(draft: string, sent: string): 'unmodified' | 'edited' | 'rewritten' {
const draftTokens = new Set(tokenize(draft))
const sentTokens = new Set(tokenize(sent))
const intersection = [...draftTokens].filter(t => sentTokens.has(t))
const overlap = intersection.length / Math.max(draftTokens.size, sentTokens.size)
if (overlap > 0.95) return 'unmodified' // Trust++
if (overlap > 0.50) return 'edited' // Trust neutral
return 'rewritten' // Trust--
}
Trust score uses exponential decay with 30-day half-life. High-trust agents can auto-send drafts.
| File | Purpose |
|---|---|
packages/core/src/slack/approval-blocks.ts | Block Kit message builder |
packages/core/src/slack/client.ts | Slack WebClient singleton (lazy init) |
packages/core/src/inngest/workflows/request-approval.ts | Approval request workflow |
packages/core/src/inngest/workflows/execute-approved-action.ts | Execute after approval |
apps/slack/app/api/slack/interactions/route.ts | Button click handler |
apps/slack/lib/verify-signature.ts | HMAC signature verification |
packages/database/src/schema.ts | ApprovalRequestsTable, ActionsTable |
| Variable | Purpose |
|---|---|
SLACK_BOT_TOKEN | Slack WebClient authentication |
SLACK_SIGNING_SECRET | Request signature verification |
SLACK_APPROVAL_CHANNEL | Channel ID for posting approvals |
For full details, see:
docs/support-app-prd/66-hitl.mdxiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience