Install in an Expo app
This page takes an Expo app from nothing to a terminal that can read its screen. The steps were verified on a new Expo SDK 57 project (React Native 0.86) that uses expo-router, on the iOS simulator and on the Android emulator.
Expo needs three things that a React Native CLI app does not, and each has a reason:
- A development build. The agent contains native code, and Expo Go only runs the native code it was shipped with. lynkeus does not work in Expo Go.
- An entry file of your own. An expo-router project has no
index.js, and the agent has to be mounted before the router loads. - A second name for the token. Expo only passes the app environment variables that start with
EXPO_PUBLIC_.
If your app does not use Expo, follow Install in a React Native app instead.
Before you start
Section titled “Before you start”- An Expo project that you can build locally: Xcode for iOS, Android Studio for Android.
- Node 22 or newer.
- React Native 0.76 or newer with the New Architecture enabled.
No config plugin is needed: the agent’s native module is autolinked. The Babel plugin that a React Native CLI app uses to pass the token (lynkeus-agent/babel) is not needed either, because Expo has its own way to pass a variable to the app.
1. Add the packages
Section titled “1. Add the packages”The agent goes into the app; the CLI stays on your machine.
npx expo install lynkeus-agentnpm install --save-dev lynkeusKeep both on the same version. The CLI prints a warning when it attaches to an agent from a different release.
2. Mount the agent
Section titled “2. Mount the agent”lynkeus init mounts the agent from the app’s entry, and knows what an Expo project needs:
npx lynkeus initIn an expo-router project you should see:
wrote index.js (mounts the agent, then loads expo-router) and pointed package.json's main at itExpo: the agent has native code, so it runs in a development build (npx expo run:ios, npx expo run:android), not in Expo GoExpo: start the dev server with EXPO_PUBLIC_LYNKEUS_TOKEN set to the same value as LYNKEUS_TOKEN; Expo passes the app only variables with that prefixThe first line is what it changed. The two lines that start with Expo: are reminders of steps 3 and 4 below.
In an Expo project that already has an entry file (index.js, index.tsx or index.ts), init adds the auto() call to that file instead, after its last import, and reads the token from EXPO_PUBLIC_LYNKEUS_TOKEN there too.
What it does, and doing it by hand
Section titled “What it does, and doing it by hand”An expo-router project starts from expo-router/entry, named as "main" in package.json, so it has no entry file of its own. init gives it one that mounts the agent first and then loads the router.
That is an index.js at the root of the project:
import { auto } from 'lynkeus-agent/auto';
auto({ token: process.env.EXPO_PUBLIC_LYNKEUS_TOKEN });
import 'expo-router/entry';And "main" pointing at it:
{ "main": "index.js"}The order in index.js matters. auto() wraps the app’s root component, so it has to run before the router registers that component.
auto() runs only in development by default (enabled defaults to __DEV__). To gate it further, pass enabled yourself, for example to keep the agent off in a development build that points at production.
If your project already has an index.js (it does not use expo-router, or you added an entry earlier), the auto() call goes there, before the app is registered.
3. Set the token under both names
Section titled “3. Set the token under both names”The token is a shared secret: the app is built with a value, the CLI presents the same value, and the agent ignores a driver that presents anything else.
Two programs read it, under two names:
| Who reads it | Variable | Why |
|---|---|---|
| The app | EXPO_PUBLIC_LYNKEUS_TOKEN |
Expo only inlines variables that start with EXPO_PUBLIC_. |
| The CLI | LYNKEUS_TOKEN |
The CLI always reads this name. |
Both must hold the same value. Export both in the shell where you build the app and start the development server, and export LYNKEUS_TOKEN in any other shell where you run lynkeus:
export EXPO_PUBLIC_LYNKEUS_TOKEN=some-local-secretexport LYNKEUS_TOKEN=some-local-secretThe app’s copy is read when the JavaScript is bundled, so it must be set before the development server starts.
4. Generate the native projects and build
Section titled “4. Generate the native projects and build”prebuild generates the ios and android folders with the agent’s native module linked. run:ios and run:android compile a development build and install it.
npx expo prebuildnpx expo run:iosOr, for Android:
npx expo run:androidYou should see the app open on the simulator or emulator, as a development build of your own app rather than inside Expo Go.
You build once. After that, the development server is enough for JavaScript changes; rebuild only when you add or update a package with native code, lynkeus-agent included.
5. Verify from the terminal
Section titled “5. Verify from the terminal”With the app running, from the project’s folder:
npx lynkeus screenYou should see a line on standard error saying the app attached, and then the screen. The first line is the route and, in parentheses, the chain of navigators that leads to it:
index (__root > index)Below it comes one line per element a user could see or touch, which depends on your app. The first line is the part to check: index is the route as expo-router names it, and __root > index is the path to it. Seeing them means the agent found the router’s navigation container, so routes are recognized.
Installation is complete. Continue with Your first test.
How it works
Section titled “How it works”auto() asks React Native to wrap whatever root component is registered later. When expo-router registers its root, the agent is already around it. expo-router is built on React Navigation, so the agent finds its navigation container the same way it does in any React Navigation app, and reports the routes expo-router derives from your app/ folder.
From there everything is the same as in a React Native CLI app: the agent dials out to port 8123 on your machine (localhost from the iOS simulator, 10.0.2.2 from the Android emulator), and each npx lynkeus … command listens there, presents the token, and runs.
What is different in an Expo app
Section titled “What is different in an Expo app”Two things behave differently from what the rest of the documentation shows. Both are covered in more detail in Expo.
Route names are file names, in lowercase. The examples elsewhere use React Navigation names such as Settings.Main. expo-router names a route after its file: index, explore. lynkeus nav index works with any name. lynkeus wait and lynkeus see, however, take either a route or a target in the same position, and read a lowercase word as a text to look for. Say that it is a route with the route: prefix: npx lynkeus wait route:index, or app.wait: "route:index" in a case. The prefix is available since 0.3.0.
Native tabs are not elements. The tab bar of the newest Expo template is drawn by the system, not by React Native. Its tabs are not listed by lynkeus screen and cannot be pressed by name. Move between tabs with lynkeus nav <route>.
Common problems
Section titled “Common problems”The CLI never attaches
Section titled “The CLI never attaches”✗ No app attached on 127.0.0.1:8123 within 15000ms. Is a dev build running with <Lynkeus>?Check, in this order:
- The app is a development build, not Expo Go. In Expo Go the agent’s native module does not exist.
package.jsonhas"main": "index.js", andindex.jscallsauto()before it importsexpo-router/entry.npx lynkeus initsets both up.- The development server was started with
EXPO_PUBLIC_LYNKEUS_TOKENset. A value exported afterwards is not in the bundle. When the app’s log showslynkeus: the app passed a token and it is empty, this is the cause, or the entry reads the variable without itsEXPO_PUBLIC_prefix. EXPO_PUBLIC_LYNKEUS_TOKENandLYNKEUS_TOKENhold the same value. A mismatch looks exactly like no app at all.
The screen reads, but a press fails
Section titled “The screen reads, but a press fails”Lynkeus native module unavailable: debug builds only, and the app must be rebuilt after adding the packageReading the screen is done in JavaScript; touching it needs the native module. This message means the JavaScript is new but the installed binary is not. Run npx expo prebuild and npx expo run:ios (or run:android) again.
A tab cannot be pressed
Section titled “A tab cannot be pressed”If the tab bar does not appear in lynkeus screen, it is a native tab bar. Use lynkeus nav <route>.
Next steps
Section titled “Next steps”- Your first test: drive the app by hand, then write and run a case.
- Expo: what is specific to Expo, and its limits.
- Concepts: the vocabulary used in the rest of the documentation.