Manifest mode — declaring screens the analyzer cannot read
appilots sync builds the application map by reading your source for React
Navigation: a <NavigationContainer> and <Stack.Screen> / <Tab.Screen>
declarations. That covers most React Native apps, but not all of them.
If your app does not use React Navigation, the analyzer finds nothing. The
most common case by far is Expo Router — the default for
npx create-expo-app — which declares routes as files, not as JSX elements the
analyzer can see. A file-based router, a custom navigator, or a web app driven
by something other than React Router all land in the same place.
Since the map is what tells the agent which screens exist and how to reach them, an empty map means an agent that cannot navigate anywhere.
appilots generate and appilots sync exit non-zero when the map has zero
screens, and the error names both likely causes. Previously the sync succeeded
and uploaded an empty map, which made the dashboard look integrated when it was
not. If an empty map is genuinely what you want, pass --allow-empty.
The escape hatch
Declare the screens yourself in appilots.manifest.json at the project root:
{
"version": "1",
"screens": [
{
"name": "ItemList",
"description": "Lista os itens do estoque",
"route": "/itens",
"actions": [{ "id": "new-item", "type": "navigate", "label": "Novo item" }]
},
{
"name": "ItemForm",
"description": "Cadastra ou edita um item do estoque",
"route": "/itens/novo",
"forms": [
{
"id": "item",
"fields": [
{ "id": "sku", "label": "SKU", "type": "text", "required": true },
{ "id": "model", "label": "Modelo", "type": "text", "required": true }
]
}
],
"actions": [{ "id": "save", "type": "submit", "label": "Salvar", "effect": "create" }]
}
],
"navigation": {
"initialScreen": "ItemList"
}
}
Screens here use the same shape the analyzer produces, so a manifest and analyzer output merge without translation. That matters for two reasons:
- You can declare only what the analyzer missed. The manifest is merged on top of whatever was found, not instead of it — a hybrid app declares its odd screens and lets the analyzer keep the rest.
- A fully declared app needs no React Navigation at all.
Point at a different file with manifestPath in .appilotsrc.
A manifest that exists but does not parse — bad JSON, a screen missing name —
is a hard error, not a skipped file. A typo must not quietly contribute zero
screens; that is the same silence this page exists to remove.
What the manifest does not do
It declares structure, not runtime state. The agent still reads the live
screen through the SDK's snapshot, so a manifest does not remove the need for
AppilotsProvider, and it does not make the SDK track the current screen.
Screen tracking is separate: AppilotsNavigationContainer handles it for React
Navigation. Without React Navigation, call setCurrentScreen() yourself when
your router changes route — otherwise currentScreen stays null and the agent
does not know where it is, even with a perfect manifest.
import { setCurrentScreen } from '@appilots/sdk';
import { usePathname } from 'expo-router';
function ScreenTracker() {
const pathname = usePathname();
useEffect(() => {
// The name must match the screen's `name` in your manifest.
setCurrentScreen(routeNameFor(pathname));
}, [pathname]);
return null;
}
The peer dependency warning
@appilots/sdk declares @react-navigation/native as a peer dependency,
because that is the path the SDK is built around. If you are driving Appilots
from a manifest instead, your package manager will print one unmet-peer warning
on install. That is expected, and it is deliberate: the warning is there so the
much larger group of people who meant to use React Navigation find out at
install time rather than after a clean sync produced an empty map.