Skip to main content

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 a testID).
  • Give it a clear, unambiguous label — that's what the agent reasons about.
  • Declare effect + riskLevel (and destructive/requiresConfirmation where applicable) in registerScreen.
  • Re-run appilots sync so 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.