Skip to content
Ryno

Send a deep link

Pick a device, run a request, and read what came back.

You have a workspace open and a device listed. What is left is the part Ryno was built for: write one link, send it, and read an answer you can trust. This page goes from an empty request to a link on a screen, and it spends most of its length on the last step, because that is where a testing tool earns its keep.

Write your first request

Pick New request in the sidebar, or press T from anywhere in the window. A request lands in collections/, inside your workspace, as one small file you can read, search and review like the rest of your project.

Give it a name, then click into the URL field and type the link you want to test. Press L to put the cursor there without reaching for the mouse, and S to write the file.

A link is made of a scheme, a host and a path — myapp://profile/42, or https://example.com/user/42 for a link that also has to work from a browser. Anything you type after a ? is folded into the query parameters table below, and stays in step with it from then on.

Wrap a name in double braces and Ryno fills it in from the environment you have selected: https://example.com/user/{{userId}}. The braces are highlighted as you type, and the environment you are on is switched with E.

Underneath the field sits the Resolved deep-link panel, which shows the link as it will actually be sent — braces filled in, disabled parameters gone, values encoded. It is rebuilt on every keystroke, so what you read there is what leaves the machine. Copy it with the button in its header, or with C.

A name no environment defines resolves to nothing at all. Ryno does not guess, and it does not stop you either: the environment panel names the braces it could not fill and asks you to define them, and the resolved link shows the hole they left. Values you keep out of your repository work the same way — an environment file whose name ends in .env.local.json is read like any other, and stays on your machine.

Turn a query parameter off without deleting it

The Query params tab holds one row per parameter: a checkbox, then Key, Value and Description. Add rows as you need them.

Unticking a row is the point of the tab. The parameter stays in the file, stays visible in the table, and is dropped from the resolved link — so it is also absent from what reaches the device. That is how you test the same link with and without a parameter without retyping either version, and how a colleague reading the file still sees the parameter you were trying. A row with no key is dropped the same way, which is why a half-typed row never breaks the link under your fingers.

Description is for you and your team. It is written into the file and travels with it, and it never reaches the link nor the command Ryno runs. Say what a parameter is for, and nothing you write can change what is sent.

Name the moving parts of a path

A path segment written :name — myapp://user/:userId/orders — becomes a row in the Path params tab, with the same Name, Value and Description columns. The rows are read from the path itself, so editing a row can never change the URL. Rename a segment and an empty row takes its place: the value you had typed is set aside rather than deleted, and comes back if you put the old name back.

A value may itself hold {{braces}}, which are filled in from the environment like everywhere else. A segment left without a value is called out next to the unfilled braces, because an empty segment in a resolved link is the kind of mistake that looks like an application bug for twenty minutes.

Aim at a device, and the editor follows

The third tab belongs to the platform of the device you have selected. Select an Android phone and it is Android intent; select a simulator or an iPhone and it is iOS launch. There is only ever one, on purpose: the editor tells you what the next run is aiming at, rather than listing everything it could aim at.

On Android the tab holds Package, Action, Category and Flags. Only the action has a value out of the box, android.intent.action.VIEW, which is the action a browser would send. Naming a package aims the link at one application instead of letting the system route it.

On iOS the tab holds Bundle identifier, Launch mode, Launch arguments and Environment, and the launch mode is also the picker at the left of the URL field. It has three settings, and they test three different things:

Mode What it runs What it tests
OPEN simctl openurl System routing — whichever app declares the scheme or owns the domain wins. The default.
LAUNCH simctl launch · openurl A named app, started with its launch arguments and environment, then handed the link once it runs.
SAFARI simctl openurl · https The bounce from the browser, smart banner included.

On a real device there is only LAUNCH, and it runs devicectl process launch: the named app is started with the link as a payload, and routing is skipped entirely.

The text fields of the tab take {{braces}} exactly like the URL: Package, Action and Category on Android, Bundle identifier, Launch arguments and Environment on iOS. They are highlighted and offered as you type, filled in from the selected environment when the request runs, and a name the environment does not define is called out next to the request like one in the URL. That is how one request aims at {{appPackage}} and reaches the staging build in one environment and the production build in the other. Flags and Launch mode are fixed choices, and stay as written.

Launch arguments and Environment reach the app in LAUNCH mode, the only one that starts it: on a simulator each variable is set for simctl launch as SIMCTL_CHILD_ followed by its name, and the command Ryno shows says so. OPEN and SAFARI only hand a link over, so the tab marks both fields launch only.

Bundle identifier is optional on a simulator and required on a real device. A simulator can be asked to open a link and work out the rest by itself; a physical iPhone cannot, so Ryno has to name the app it hands the link to. Select a handset with the field empty and Ryno says so next to the request, before you run anything, rather than letting the launch fail with a message from Apple’s tooling that reads like a bug in Ryno.

Run it

The green Run button sits in the top bar, and does the same from anywhere in the window. It stays out of reach until a request and a device are both selected, so there is no way to fire a link at nothing.

Three answers, and only one of them is a verdict

This is the part worth reading before you need it. A finished run is reported one of three ways, and the middle one is not a shade of the first.

A link that was handled. On an Android phone, an Android emulator or an iOS simulator, the system tells Ryno whether anything claimed the link. If something did, you get Link opened and a green mark; if nothing did, you get Launch failed in red, in plain words, with what to check — and the tool’s own words folded away underneath, for when you want them. Green means what it says, here.

A link that was sent. On a physical iPhone or iPad, Apple’s tooling has no door that asks iOS to open a link. Its only door starts an app you name and hands it the link — without consulting the system, and reporting the same clean success whether the app understood the link, ignored it, or was the wrong app entirely. So Ryno refuses to call that a success. It says Link sent — RYNO can’t tell what the app did with it., in muted ink rather than green, and files the run under that same sentence. Nothing failed, nothing is confirmed, and the answer is on the phone in your hand.

A link that never left. Ryno can also refuse to run, which it does rather than send something it knows is wrong — a temporary request still carrying braces nothing can fill, for instance, since a scratch belongs to no workspace and has no environment to read from. You get Launch refused and the reason, and no link is sent.

Read what came back

Device logs sits under the editor and keeps the history of the request you are looking at. Each entry carries the exact command line that was sent and the output that came back, with a Copy button that takes both at once — which is what you paste into a bug report.

The command shown is the command run. Values are quoted before they reach the shell, so a link with an ampersand or a space in it is sent whole rather than being cut in half by the shell, and the line in the journal is the line you can paste into a terminal yourself.

Two of the options below fill this panel further: a captured extract of the device’s own log stream, attached to the run it belongs to. The panel’s menu also clears the history, either for this request or with Clear logs for all requests.

Ask whether the domain declares your app

Next to the resolved link sits a small mark: App Links when you are aiming at Android, Universal Links when you are aiming at iOS. It answers one question — does the domain in your link declare this app, for this path? Each state has its own shape as well as its own colour, so it still reads on a monochrome screen.

Click it and Ryno shows you everything the check read: what the domain declares, the signing fingerprints, the paths it covers, and how the app installed on the device compares. The mark only applies to https:// links, since a custom scheme has no domain to interrogate.

Every option, and what it starts at

The Options tab is the same list on both platforms, in four sections: Before launch, Behavior, Encoding & delivery and Diagnostics. An option the device you are aiming at cannot run is not hidden — it goes grey, keeps its stored value, and is tagged with the platform that does run it. A request is shared with your team, and a row that vanished would let the tab lie by omission to whoever opens it from another machine.

Option Default What it does
Boot simulator if needed Off Starts the selected simulator first, so a cold machine still runs. iOS only.
Force-stop app first Off Kills the app so the link arrives at a genuine cold start.
Clear app data Off Wipes the app’s data first, so the link lands on a signed-out, first-launch state. Android only.
Wait for launch On Blocks until the activity reports it is up. Android only.
Force-stop before start Off Clears the launch task only, not the whole app. Android only.
Wait for debugger Off Starts the activity paused until a debugger attaches. Android only.
Run timeout No limit Fails the run if the activity does not start in time. Android only.
Intent flags None Extra flags added to the launch intent. Android only.
URL-encode values On Percent-encodes query values; turn it off to send them raw.
Delivery method am start Send the link directly, or wrap it as intent:// the way a browser would. Android only.
Screenshot on finish Off Grabs a screenshot once the launch settles and attaches it to the run.
Capture logcat Off Attaches a recent extract of the Android log stream. Android only.
Capture os_log Off Attaches an extract of the Apple log stream, narrowed to your bundle. iOS only.

Reset to defaults at the top of the tab puts every row back to the values above.

Screenshot on finish, Capture logcat and Capture os_log are best effort. They run after the launch, and a capture that fails is dropped quietly rather than turned into a failed run: the outcome you read is the one the launch earned. So a run with no image and no log extract attached is not a run that went wrong — the absence is about the capture, and never a symptom of what the device did with your link.

Three of these look alike and are not, and confusing them costs a session:

  • Force-stop before start clears the launch task of the activity you are opening. It is the lightest of the three.
  • Force-stop app first kills the whole app before the run, as a separate command. State the app kept in memory is gone; state it wrote to disk is not.
  • Clear app data goes further still and wipes what the app stored, which puts it back in a signed-out, first-launch state. Use it when you are testing what happens to somebody opening your link for the first time, and not before.

And one trap worth naming. Run timeout has no default, and no value means no limit. That is harmless on its own, and it stops being harmless the moment you turn Wait for debugger on: the launch then waits for a debugger that may never attach, and nothing brings it back. Set a timeout whenever you pause for a debugger — ten seconds is a reasonable start.

Work down this list before suspecting the application.

Nothing runs at all. Check that a request and a device are both selected. The Run button is deliberately out of reach otherwise.

Ryno refused to launch. Read the sentence next to the request. Aiming at a physical iPhone with no Bundle identifier is the common one, and so is picking SAFARI for a link that is not an https:// link — there is no browser to bounce from.

The run failed and the journal names the tool. Copy the command and its output from the entry and run the same line in a terminal. It is the line Ryno sent, so whatever answers you there is what answered Ryno.

The link went out and the app did nothing. Check the resolved link rather than the one you typed: an unfilled {{name}} leaves a hole, and a parameter you unticked is genuinely gone. Then check the domain mark — a link the domain does not declare for that path opens in the browser rather than in the app, which looks exactly like the app ignoring it.

The run says the link was sent. That is a real iPhone, and it is not a failure. Ryno handed the link over and cannot see any further: look at the device’s screen for the answer, or reproduce the same request on a simulator, where a link nothing claims fails out loud.