SDK reference
For AI agents: this page is Markdown at https://squiggle.sh/docs/reference/sdk.md. Every docs page is listed at https://squiggle.sh/llms.txt, and all of them are one file at https://squiggle.sh/llms-full.txt.
Everything your app can import from @squiggle.sh/react-native. Only the import is required; the rest tells Squiggle things it can't work out on its own.
The import
import '@squiggle.sh/react-native';It must be the first line of your entry file, before anything loads React. squiggle init puts it there. See Set up your app.
withSquiggle
From @squiggle.sh/react-native/metro. It wraps your Metro config so that a development build times each module your app evaluates while it starts. The report card uses those times to name the screens your app loads at startup though it doesn’t open on them. It adds nothing to a production bundle. squiggle init adds it.
const { getDefaultConfig } = require('expo/metro-config');const { withSquiggle } = require('@squiggle.sh/react-native/metro');const config = getDefaultConfig(__dirname);module.exports = withSquiggle(config);Wrap the config your file exports last. It changes the config you pass and returns it, so it works inside or outside other with… wrappers. After you add it, restart Metro with --clear. Until the bundle is rebuilt, the report card’s startup row stays pending.
<Squiggle>
Wrap your root component in it. It tracks the current screen, feeds the heatmap every touch, and adds the on-device overlay in development builds.
import { Squiggle } from '@squiggle.sh/react-native';import { Stack, useNavigationContainerRef } from 'expo-router';export default function RootLayout() { return ( <Squiggle navigationRef={useNavigationContainerRef()}> <Stack /> </Squiggle> );}| Prop | What it does |
|---|---|
navigationRef | Your navigation container ref: useNavigationContainerRef() with Expo Router, or your NavigationContainer ref with React Navigation. Without it, Squiggle can’t tell which screen is showing. |
overlay | Show the on-device overlay. Default true. It only ever draws in a development build. |
touchCapture | Send every touch to the heatmap. Default true. |
touchDetectors | Check every touch for rage taps and dead zones, not only touches on gesture-handler controls. A touch counts as a tap only if it lifts where it went down, so scrolls never count. Default true. |
Keep GestureHandlerRootView at your root as usual, with <Squiggle> inside it.
defineAction
Names something a user does. Each call is timed until it finishes, and recorded as succeeded or failed. Named actions show up in feature usage and funnels, and an agent can call one by name.
import { defineAction } from '@squiggle.sh/react-native';const addToCart = defineAction('add_to_cart', (product: Product) => cart.add(product));Call addToCart(product) wherever you called the original. Errors are passed through unchanged.
identify and tag
identify sets who is using the app, for the users and retention views. tag adds key-value data to whatever the user is doing right now. Add appVersion (or release) to the traits to get release markers on the trend charts and to compare releases with squiggle product state --drift.
import { identify, tag } from '@squiggle.sh/react-native';identify('user_8f2a', { plan: 'pro', appVersion: '1.4.2' });tag({ experiment: 'checkout-v2', variant: 'B' });Traits and tags must be plain data under 8 KB. Larger ones are dropped, with a warning in development.
agentHint
Says what a control is, in props it spreads onto the component. The most useful one is destructive: an agent can’t press a destructive control without asking you first.
import { agentHint } from '@squiggle.sh/react-native';<Pressable {...agentHint({ id: 'delete-account', destructive: true })}> <Text>Delete account</Text></Pressable>;| Field | What it does |
|---|---|
id | Becomes the testID, so flows can find the control by it. |
role | button, link, tab, switch, checkbox, radio or menuitem, when the component doesn’t have one. |
label | The accessibility label. |
destructive | The control logs out, deletes or pays. Agents need your approval to press it. |
flow | The journey the control belongs to, such as checkout. Groups controls by purpose. |
logEvent
Writes one structured event. A flow can then assert on a field of the event, which keeps working when the wording of a log message changes.
import { logEvent } from '@squiggle.sh/react-native';logEvent('order.submitted', { orderId: 'ord_2291', total: 48.5 });For fields on every event, or your own output, use createRunLogger and consoleJsonSink from the same package.
Save and restore your app’s state
squiggle save checkout-3-items records the screen your app is on and the data behind it. squiggle load checkout-3-items puts both back, in under a second, instead of tapping through screens to rebuild them. In a debug build this needs no code: Squiggle captures the navigation state, every Zustand and Legend State store, and every MMKV instance. A release build is captured only when it was built with EXPO_PUBLIC_SQUIGGLE_STATE_ANALYTICS=1 and its user hasn’t withdrawn consent. In Studio, the device panel’s Capture menu saves and loads the same slots; see Drive the simulator.
- A save holds plain data. Functions, class instances,
Datevalues and cycles are left out, andsquiggle savelists each one it skipped. - State that lives inside a component, such as
useState, a scroll position or a running timer, is not saved. - MMKV data is copied on the simulator itself, so it loads only on the simulator that took it. Loading the save on another simulator, or after you reset the app’s data, restores the screen and the stores, leaves every MMKV instance as it was, and reports the load as failed, naming the MMKV instance. Saving MMKV needs react-native-mmkv 4 or later (
createMMKV). An encrypted MMKV instance is never copied, andsquiggle savelists it as left out. - A save is checked against your app before any of it is restored. If the app has changed since, such as a store with a renamed or added top-level key, a store that no longer exists, or a screen that was removed,
squiggle loadrefuses the whole save, names what changed, and leaves your app as it was. Save again. - Stores are saved under their names (see Store names). A save that names a store by its creation order, such as
store:zustand-2, is refused once the Babel plugin names that store. Save again.
Saves live in .squiggle/agent/fixtures/ in your project and never leave your machine.
Store names
Saves and state samples name each store. With the Babel plugin, a store declared at the top level of a module is named after its variable: export const useCart = create(…) is zustand:useCart, and a Legend State store is legend: and its variable. The name stays the same from release to release and across a Fast Refresh.
- A store the plugin can’t name, such as one created inside a component or in a build without the plugin, is named by the order it was created in, such as
store:zustand-3. That name moves when you add a store. - Two stores declared under the same variable name in different files share it: the second one created is
useCart~2. - After you upgrade Squiggle, restart Metro with
--clear. Until the bundle is rebuilt, stores keep theirstore:zustand-3names.
Turn on state samples
State samples feed squiggle product state, --where and friction --state. Your app sends them only when you build it with EXPO_PUBLIC_SQUIGGLE_STATE_ANALYTICS=1 (or true), in a debug build as much as a release one. Saves don’t need it in a debug build.
The value is read when the bundle is built. After you change it, clear the bundler’s cache for the next release build, for example npx expo export --clear: without it, the build can keep the value it was built with before.
With the variable set, also tell Squiggle what each of your users chose. The default is allowed, and Squiggle stops capturing the moment you pass false:
import { setStateAnalyticsConsent } from '@squiggle.sh/react-native';// Call it wherever your app records the user's analytics choice.setStateAnalyticsConsent(settings.analyticsAllowed);| Consent | What it means |
|---|---|
off | The app was built without the variable. Nothing is sampled, whatever you pass. |
granted | Samples are sent. The default with the variable set. |
revoked | You passed false. Nothing is sampled until you pass true, and a release build stops capturing saves too. |
setStateAnalyticsConsent returns the consent now in force, getStateAnalyticsConsent() reads it, and onStateAnalyticsConsent(listener) calls you when it changes and returns a function that stops it.
With consent, Squiggle sends a sample of your app’s state when the screen changes, and half a second after your stores stop changing, or 2 seconds after they started if they never stop. It sends at most two a second, and only when something changed. A sample holds shapes and counts, never values: the current route, how many items an array holds, how many keys an object has, whether a boolean is true, a number’s sign and order of magnitude, and a string’s length rounded down to a power of two. It reads two levels into each store.
- A field whose name looks like a secret, such as
password,accessToken,sessionIdorpin, is left out entirely. - An object keyed by data, such as emails or ids, is sent as its number of keys only.
- MMKV is sent as each instance’s number of keys.
Each value becomes a path, such as zustand:useCart.items#len. See State paths for every kind.
To send the values of a field such as a theme or a plan tier, list it by its store name. Each call replaces the list:
import { setStateAnalyticsAllow } from '@squiggle.sh/react-native';// export const useSettings = create(() => ({ theme: 'dark', plan: 'pro' }));setStateAnalyticsAllow(['zustand:useSettings.theme', 'zustand:useSettings.plan']);A listed field stops sending values for the rest of the session once it has shown more than 16 different ones, and a value longer than 64 characters is never sent; in both cases its length is sent instead.
registerFixtureSource
Adds a store Squiggle can’t see on its own to every save, or replaces how one is saved: a source you register wins over Squiggle’s own source of the same name. It returns a function that unregisters the source.
import { registerFixtureSource } from '@squiggle.sh/react-native';registerFixtureSource('cart', { capture: () => cartStore.getState().items, restore: (items) => cartStore.setState({ items }), misfit: (items) => Array.isArray(items) ? undefined : 'the cart is no longer a list',});misfit is optional. Return a reason when a save no longer fits your store, for example one taken before you changed its shape, and undefined when it does. Squiggle asks every source before restoring any, so one misfit refuses the whole save. Keep the name stable: a save that names a source your app no longer registers is refused the same way.
| Field | What it does |
|---|---|
capture | Returns the store’s data as plain data, or a promise of it. Return undefined to leave this source out of this save. |
restore | Puts saved data back. Return false or throw to refuse it; the load reports that source as failed. |
misfit | Optional. Why saved data no longer fits, or undefined when it does. |
skipped | Optional. The paths the last capture left out. squiggle save lists each one as name:path. |
defineTunable
Names a number an agent may change while the app runs, such as a page size or a debounce, and the range it may take. An agent lists and sets it with the tune tool, so a value can be chosen by measuring it instead of by feel. useTunable reads it in a component and re-renders when it changes; get() reads it anywhere else.
import { defineTunable, useTunable } from '@squiggle.sh/react-native';const pageSize = defineTunable('feed.pageSize', { default: 20, min: 5, max: 100, step: 5, description: 'rows fetched per page',});function Feed() { const n = useTunable(pageSize); // …}defineTunable throws when step isn’t positive, when min is above max, or when the default is outside the range or off the grid. The app refuses any name it didn’t declare, and any value that isn’t a finite number, is outside the range or is off the step grid, and the agent sees why, such as tune: 'feed.pageSize': 7 is not on the step-5 grid from 5. A value an agent sets is dropped on reload. A Fast Refresh that declares the tunable again keeps the value only while the new range allows it. Nothing else in your app can be changed this way; see Safety and approvals.
tokens lists the values your design system already uses for the number, such as your spacing scale. A layout repair (--repair) moves each value it chooses onto the nearest token that still works, and flags one it can’t. Each token must be a value the tunable allows, or defineTunable throws.
Environment variable
| Variable | What it does |
|---|---|
EXPO_PUBLIC_SQUIGGLE_WS_URL=ws://localhost:<port> | Where the app connects to Studio or the CLI. Unset, an iOS simulator app connects to ws://localhost:7777. Set it when you move the port (squiggle uses the same port from port in config.jsonc or SQUIGGLE_PORT), or to your Mac’s address for a physical device. |
EXPO_PUBLIC_SQUIGGLE_WS_URL=off | Stop the app connecting to Studio or the CLI on its own, so the on-device overlay can run. Studio and squiggle flow still connect when they launch the app. |
EXPO_PUBLIC_SQUIGGLE_STATE_ANALYTICS=1 | Send state samples, in any build, subject to each user's choice, and allow saves in a release build. Debug builds capture state for saves either way. |
Libraries Squiggle instruments
If your app uses one of these, Squiggle records it automatically. You never need to install one for Squiggle.
| Library | What Squiggle records |
|---|---|
zustand (4+) | Store changes. |
@react-navigation/native | The current screen. |
@tanstack/react-query (5+) | Query state. |
react-native-gesture-handler (2+) | Gestures. A pan, pinch or rotation also keeps its touch path (up to 32 points), and a pan says what it looked like: a swipe to dismiss, a pull to refresh, a drag to reorder or a scrub. |
react-native-worklets (0.8+) | Gestures that run on the UI thread, with their touch path kept and scored there rather than sent to JavaScript on every move. |
react-native-mmkv (2+) | Reads and writes. Needs the Babel plugin. Saves need 4+. |
@react-native-async-storage/async-storage | Reads and writes. Needs the Babel plugin. |
op-sqlite, expo-sqlite | Queries. Needs the Babel plugin. |
@legendapp/state (3+) | Observable reads and writes. Needs the Babel plugin. |
Probes in a development build
A development build also runs probes: rules you write in squiggle probe when that the app checks while you use it, with no rebuild. There is nothing to set up, and a release build never runs one. See Watch for a condition inside the app.