Launching soon. Glion is in private preview; early access is by request.

Docs

The first ten minutes

The six setup steps, why they come in that order, and why the wizard cannot report a step done that is not.

From the product's docs/ONBOARDING.md, last changed there on , exported at commit 27c7e104662b.

This document is about what happens between installing Glion and having an agent do something useful. It is the part of the product a new operator judges it by, and for a while it was the worst part of it.

What it used to be

A fresh install put you on the Fleet screen, which was empty, above a checklist of four things you had not done. Each item was a link to a different screen. Nothing said which to do first, nothing carried you from one to the next, and nothing was remembered — navigate away and the checklist started again from the beginning, with no idea where you had got to.

Worse, the order it implied was wrong. The first item was “give a provider an API key”, which meant the first thing Glion asked of a stranger was money, before it had shown them anything at all. The one thing it can do with no key — look at your own network and tell you what is on it — was three screens away under a menu item called Discovery.

What it is now

One route, /setup, six steps, and the order is the product’s real dependency graph rather than a list:

step what it asks for needs a key
1 Your network confirm a subnet, then scan it no
2 What is yours keep or set aside what was found no
3 Reach them one token per service no
4 A model one provider key —
5 Your team four agents shaped by what was found yes
6 First task one task, grounded in the finds yes

The two steps that need no credentials come first. By the time Glion asks for an API key it has already told the operator that there are twenty-six services on their network, named them, and connected to several — which is the whole argument for the product, made with their own infrastructure rather than a screenshot.

Every step is skippable, every skip is recoverable, and you can walk backwards freely. A step is locked only when it genuinely cannot be attempted, and when it is locked it says why in words meant for a person: “Each agent needs a model to run on. Add a provider key first.”

The one idea it rests on

server/src/onboarding/journey.ts splits every piece of state into one of two kinds, and the split is what stops the wizard from lying.

Derived — whether a step is done. Read from the database on every request and never stored. A provider key added on the Models screen counts. A team built from the Discovery screen counts. There is no flag anywhere that can report a step finished when the state says otherwise, which is the failure mode of every setup wizard that has ever told a user they had completed something they had since undone.

Declared — the cursor, and what was skipped. These are intentions. “I do not want to connect anything yet” appears in no table and cannot be inferred from one, so it is the operator’s to say and Glion’s to remember. They are the only two things written down.

There is exactly one step whose completion is an acknowledgement rather than a state change. Confirming which discovered services are actually yours changes nothing if they all are — there is no row to write. Pressing Continue is the action, so the cursor moving past that step is recorded. That is still a real gesture by a person, not a guess, and it is read by that step alone.

The precision matters more than it looks. An earlier version marked every step before the new cursor, which meant clicking step four in the stepper marked “what is yours” as done — a step the operator had never opened, whose entire purpose is for them to look at a list. Only a cursor move of exactly one step forward acknowledges anything, because that is the only move Continue makes; jumping the stepper and going back acknowledge nothing. acknowledge() is a pure function and server/test/journey.test.ts pins all four cases.

A consequence worth stating: an instance that has been running for six months is never greeted as though it had just been installed, because status is derived from the same real state. Nothing was migrated to make that true.

The scan says where it has got to

The other half of the problem was the scan itself. It was one POST that answered after up to a minute of silence — the longest unexplained wait in the product, and the very first thing anyone asks Glion to do. More than one operator concluded it had hung, reloaded the page, threw the result away and put the packets on the network a second time.

server/src/discovery/job.ts runs the same sweep in the background and publishes progress on the console’s existing live stream as discovery messages. The job outlives the request that started it, so a browser that reloads mid-scan reattaches through GET /api/discovery/scan/current instead of watching it vanish, and a console whose SSE is being buffered by a proxy polls the same endpoint instead.

Two details in there are less obvious than they look.

Only one scan runs at a time. Two concurrent sweeps at concurrency 256 put twice the packets on someone’s network for no extra information, and the obvious way to cause that is to double-click Scan. A second request while one is running returns the running job.

The bar only ever moves right. A scan is three legs — a liveness pass over the range, a fingerprint pass over what answered, then asking each open port what it is — and the operator has one bar. Each leg’s size becomes knowable only when the one before it ends, so the denominator is an estimate that gets corrected twice. Left alone, each correction snapped the bar backwards at exactly the moment the scan got interesting, which reads as a fault in the network being measured rather than in the estimate. The numerator carries the finished legs forward with it, mergeProgress refuses to lose ground, and server/test/scan-job.test.ts pins it.

The first version of this fixed only the first correction, and an adversarial review found the bar still resetting at the second one — one phase past the guard written to prevent it. Worth remembering: a guard against a class of bug is not the same as a guard at every place the class occurs.

The counts say what they can actually know. live_hosts is machines that answered; found is services identified. The sweep knows the first and cannot know the second — what it has are open ports — so it leaves found alone. Filling it from the port count made the figure climb through the sweep and then fall to zero the moment identification began, and a count that falls reads as something going wrong.

The audit row is still written before the first packet, by the route rather than the job, because the route is the only thing that knows the requesting address. Both scan endpoints go through the same announce helper; neither can skip it.

The API

GET  /api/onboarding/journey            the six steps, derived + declared
POST /api/onboarding/journey            { cursor | skip | unskip | complete | dismiss | restart }
POST /api/discovery/scan/start          { cidr } -> { job }, returns immediately
GET  /api/discovery/scan/current        the job in flight, or the last one
POST /api/discovery/scan                the same sweep, awaited (unchanged)

restart deliberately clears the intentions and nothing else. It does not un-discover a network or un-key a provider — those are real state, and a wizard that could delete them by being reopened would be a trap.

Where the code is

server/src/onboarding/journey.ts the six steps, and the derived/declared split
server/src/discovery/job.ts the background sweep and its progress
server/src/routes/discovery.ts the endpoints above, and announce
web/src/screens/Setup.tsx the wizard, and the resume card the Fleet shows
web/src/components/ConnectCard.tsx one service’s connection, shared with /connect
web/src/styles/wizard.css the stepper, the progress bar, the finish panel
server/test/journey.test.ts the ways the derived/declared split could break
server/test/scan-job.test.ts the range refusal, and the monotonic bar

The three security checks — TLS, a console password, an encryption key — live in journey.ts and are shown in two places from that one definition. They are deliberately not a step of the wizard: a console reachable over plain HTTP with no password is not something an operator may skip past, it is a standing fact about the deployment, and the Fleet screen keeps saying so until it is fixed.