Custom Actions
Most agent behavior is covered by the built-in
action types: navigate, form_fill,
ui_interaction, scroll_list, and confirm. But real apps have buttons
that do more than move between screens — they call APIs, start payments,
delete records. This guide shows how to expose that logic to the agent
without giving up control.
The core pattern: the agent presses, your handler decides
The agent never runs your business logic directly. It presses a registered
target, and your own onPress handler does whatever it always did. That
means exposing custom behavior is mostly about registering the trigger and
describing it well:
import { useAppilotsTarget } from '@appilots/sdk';
function SubscriptionScreen() {
const renewSubscription = async () => {
await api.renewSubscription(); // your logic, unchanged
};
useAppilotsTarget('renewSubscription', {
onPress: renewSubscription,
label: 'Renew subscription',
screen: 'Subscription',
});
return <Button title="Renew" onPress={renewSubscription} />;
}
From the agent's perspective this is a ui_interaction press. From yours,
it's the same code path a human tap uses — validation, error handling, and
side effects all stay in one place.
Describe the action's intent and risk
Pressing "Renew subscription" charges money. The agent should never do that
silently. Declare the action's semantics in
registerScreen so a confirmation gate is inserted
before it runs:
import { registerScreen } from '@appilots/sdk';
registerScreen({
name: 'Subscription',
title: 'Subscription',
actions: [
{
id: 'renewSubscription',
label: 'Renew subscription',
type: 'button',
effect: 'write',
riskLevel: 'high',
requiresConfirmation: true,
},
{
id: 'cancelSubscription',
label: 'Cancel subscription',
type: 'button',
effect: 'destructive',
riskLevel: 'high',
destructive: true,
},
],
});
With this metadata in place, the user sees an explicit confirmation card in
the chat before either action executes — and confirm steps always require a
real tap, regardless of the chat's autoExecute setting. The SDK enforces
this locally from your registerScreen metadata even if the uploaded MCP
document is stale, as a safety net.
The CLI's static analysis also detects many of these on its own: handler
names with mutation verbs (delete..., pay..., renew...) and
type: 'submit' actions are flagged as mutating, and the
metadata lint warns when a mutating action is
missing effect/riskLevel. Run appilots sync --strict-metadata in CI to
make those warnings blocking.
When the custom action type appears
The wire-level action type custom exists for actions that don't map onto
the built-in payloads. In practice you rarely need to think about it: the
recommended integration path is always "register a target + describe it" as
above, because it keeps your app in control of what actually executes. If
you're consuming the API directly
(no SDK), treat custom actions as opaque { id, type, payload } envelopes:
execute them however your client sees fit and report the outcome via
/agent/continue.
Checklist
- Register the trigger (
useAppilotsTarget, a HOC, or auto-tracking with atestID). - Give it a clear, unambiguous
label— that's what the agent reasons about. - Declare
effect+riskLevel(anddestructive/requiresConfirmationwhere applicable) inregisterScreen. - Re-run
appilots syncso the MCP document reflects the new action. - Test the risky path: ask the agent to perform the action and verify the confirmation card appears before anything happens.
Related
- Agent actions — the full action model
- Metadata quality — the lint that keeps risk metadata honest
- Security best practices — permissions and gating strategy