Registering Elements
Before the agent can fill a field, press a button, flip a toggle, or move a slider, the underlying component has to be registered in the component registry — the runtime index the executor uses to find and drive it when it receives a form_fill or ui_interaction action.
There are three ways to register a component, from lightest to heaviest integration effort. All three feed the same underlying registry, so you can mix and match across your app.
1. Zero-config auto-tracking
enableAppilotsAutoTracking() patches React.createElement (and the JSX runtime) so that TextInput, Switch, TouchableOpacity/Pressable, and list components (FlatList, SectionList, VirtualizedList, FlashList) auto-register — without touching component code.
Call it once at app startup, before any rendering:
import { enableAppilotsAutoTracking } from '@appilots/sdk';
enableAppilotsAutoTracking();
export default function App() {
// ...
}
If you're using .appilotsrc and the Metro plugin, this can also be turned on automatically via initAppilots() — see the autoTracking field in CLI configuration:
{
"autoTracking": {
"enabled": true,
"trackPressable": true
}
}
Identification order
Auto-tracked components are identified by the first non-empty value found, in this order:
testIDnativeID/idaccessibilityLabelnameplaceholder- A non-
"off"autoCompletevalue
If none of these are present, the input falls back to a best-effort semantic hint derived from secureTextEntry/keyboardType (e.g. "password", "numericInput") — logged with a warning, since the id wasn't authored and can silently change. If even that fails, the component is skipped and stays invisible to the agent.
Pressables need an onPress prop to register at all — a testID alone isn't enough for a TouchableOpacity/Pressable.
Add a testID to anything you want the agent to reliably find — it's the most reliable id source and the only one that won't shift if you rename a label.
Options
enableAppilotsAutoTracking({
trackPressable: true, // default: true
debug: false,
});
2. Hooks
Call a hook inside a component to register that specific instance. Each hook auto-unregisters on unmount, so you don't need any cleanup code.
useAppilotsField
Registers a text-like input.
import { useState, useRef } from 'react';
import { TextInput } from 'react-native';
import { useAppilotsField } from '@appilots/sdk';
function PlateInput() {
const [plate, setPlate] = useState('');
const plateRef = useRef<TextInput>(null);
useAppilotsField('plate', {
value: plate,
onChangeText: setPlate,
ref: plateRef, // optional — enables focus()
fieldType: 'text', // optional — hint for the executor
label: 'License Plate', // optional — human-readable name
screen: 'VehicleForm', // optional — scoped to a screen
});
return <TextInput ref={plateRef} value={plate} onChangeText={setPlate} />;
}
useAppilotsTarget
Registers a pressable — a button, card, or link.
import { useAppilotsTarget } from '@appilots/sdk';
function SubmitButton({ onSubmit }: { onSubmit: () => void }) {
useAppilotsTarget('submitVehicle', {
onPress: onSubmit,
label: 'Submit Vehicle',
screen: 'VehicleForm',
});
return <Button title="Submit" onPress={onSubmit} />;
}
useAppilotsToggle
Registers a switch/toggle.
import { useState } from 'react';
import { Switch } from 'react-native';
import { useAppilotsToggle } from '@appilots/sdk';
function PushToggle() {
const [pushEnabled, setPushEnabled] = useState(false);
useAppilotsToggle('pushNotifications', {
value: pushEnabled,
onValueChange: setPushEnabled,
label: 'Push Notifications',
screen: 'Settings',
});
return <Switch value={pushEnabled} onValueChange={setPushEnabled} />;
}
useAppilotsSlider
Registers a numeric slider. The executor clamps incoming values to [min, max] and snaps to step before calling onValueChange.
import { useState } from 'react';
import { useAppilotsSlider } from '@appilots/sdk';
function MileageSlider() {
const [mileage, setMileage] = useState(0);
useAppilotsSlider('mileage', {
value: mileage,
onValueChange: setMileage,
min: 0,
max: 300000,
step: 500,
label: 'Mileage',
screen: 'VehicleCreate',
});
return <MySlider value={mileage} onChange={setMileage} min={0} max={300000} />;
}
3. HOC factories
Wrap a shared component definition once, and every usage of it auto-registers — no per-instance hook calls needed. The id is derived from a label/title prop (camelCase-normalized) unless you pass appilotsId explicitly.
createAppilotsInput
The wrapped component must accept label, value, and onChangeText.
import { createAppilotsInput } from '@appilots/sdk';
import { TextInput } from 'react-native';
function BaseInput({ label, value, onChangeText, ...rest }) {
return <TextInput value={value} onChangeText={onChangeText} {...rest} />;
}
export const Input = createAppilotsInput(BaseInput);
// Every usage auto-registers — no extra code needed.
<Input label="Email" value={email} onChangeText={setEmail} />
// Or with an explicit id:
<Input appilotsId="userEmail" label="Email" value={email} onChangeText={setEmail} />
Adds appilotsId?, appilotsScreen?, appilotsFieldType?, appilotsEnabled?, and appilotsSensitive? props (see Marking a field as sensitive below).
createAppilotsButton
The wrapped component must accept title and onPress.
import { createAppilotsButton } from '@appilots/sdk';
import { TouchableOpacity, Text } from 'react-native';
function BaseButton({ title, onPress, ...rest }) {
return (
<TouchableOpacity onPress={onPress} {...rest}>
<Text>{title}</Text>
</TouchableOpacity>
);
}
export const Button = createAppilotsButton(BaseButton);
<Button title="Save Vehicle" onPress={handleSubmit} />
Adds appilotsId?, appilotsScreen?, and appilotsEnabled? props.
createAppilotsSwitch
The wrapped component must accept label, value, and onValueChange.
import { createAppilotsSwitch } from '@appilots/sdk';
import { Switch } from 'react-native';
function BaseToggle({ label, value, onValueChange }) {
return <Switch value={value} onValueChange={onValueChange} />;
}
export const Toggle = createAppilotsSwitch(BaseToggle);
<Toggle label="Push Notifications" value={pushEnabled} onValueChange={setPushEnabled} />
Adds appilotsId?, appilotsScreen?, and appilotsEnabled? props.
Marking a field as sensitive
A TextInput with secureTextEntry is automatically redacted everywhere: the runtime snapshot masks its value as <hidden>, the prompt sent to the model shows secure=true instead of the real value, and the persisted agent_actions payload is scrubbed if the model ever fills it.
Some inputs hold a credential but can't (or don't) set secureTextEntry — a custom-styled masked field, a third-party input component, a PIN entered via a numeric keyboard, or any field whose id/label doesn't obviously read as "password" (e.g. a generic field_4). For those, set appilotsSensitive on the input and it gets the same treatment:
<TextInput
testID="field_4"
value={pin}
onChangeText={setPin}
keyboardType="number-pad"
appilotsSensitive
/>
This works directly on any host TextInput the fiber walker sees, and on createAppilotsInput-wrapped components (as long as the wrapper forwards unknown props down to its underlying TextInput, as shown in the example above):
<Input appilotsSensitive label="Security code" value={code} onChangeText={setCode} />
data-appilots-sensitive and __appilotsSensitive are equivalent aliases, mirroring the existing appilotsSkip/data-appilots-skip/__appilotsSkip convention used to hide a subtree from the agent entirely.
Under the hood, this forces secure: true (and type: 'password') on the field's snapshot entry — every redaction layer downstream already keys off that flag, so there's nothing else to configure. This is unrelated to the mcp-generator @appilots-pii JSDoc marker, which gates permission to a whole screen rather than redacting a field's value.
Which one should I use?
| Approach | Best for |
|---|---|
| Auto-tracking | Fastest path to coverage across an existing app. Add testIDs where they're missing and you're done. |
| Hooks | Precise control over a handful of custom components, or fields that need dynamic ids/labels. |
| HOC factories | You have a shared design-system component library (Input, Button, Toggle) and want every instance to register with zero per-instance boilerplate. |
Next steps
- Hooks — the read-side hooks (
useAppilots,useAppilotsChat, etc) for building custom UI. - Navigation —
registerScreenfor screen-level metadata, which complements (and doesn't overlap with) element registration.