You are probably here because something did not do what you expected. Find the symptom, not the feature.
The 60-second check
Work from the outside in. Most “the app is broken” reports end at step 1 or 2.
- Is it deployed, and is it healthy? Run ▸ App Health. An app that serves nothing is a deploy problem, not a configuration problem.
- Is the version you are thinking of actually live? Run ▸ Releases names it. A draft you never published is not live.
- Did publishing warn you? Configuration issues are surfaced at publish. Re-read them.
- Did the thing actually run? Run ▸ Workflow Runs / Anomaly Events. “It didn’t notify me” is nearly always “it never ran” or “it ran and went to someone else”.
- Who received it? Check the recipients on the notification, not just that one exists.
- What changed? Run ▸ Audit, scoped to the engagement.
Nothing reaches my client
| What you see | What is happening | What to do |
|---|---|---|
| Sending an invoice is refused as “no way to be paid” | Stripe is not collecting payments for your firm and Settings ▸ Branding has no bank wire details, so the PDF would carry no payment instructions at all — while the client portal tells your client to pay using the details on the invoice. | Add bank wire details in Settings ▸ Branding, or connect Stripe, then send. |
| The invoice email has no pay link | The billing address on the invoice is not a portal identity. The link is withheld on purpose rather than sending a stranger a payment page. | Invite that exact address to the portal, then resend. |
| “Not yet deployed” on something I just built | Building produces a draft. Nothing reaches your client until you publish and deploy. | Ship it ▸ Publish, then Deploy. |
| My change is not showing up | Same cause: it is still a draft. | Publish and deploy. Run ▸ Releases confirms which version is live. |
| I published to Production and nothing appeared | Production can be approval-gated. The publish returns “awaiting approval” and the release does not ship until someone approves it — Staging publishes immediately, which is why the two behave differently. | Run ▸ Releases banners any release awaiting approval and links to it. Approve it there. |
| Approving is refused: “This promotion … was planned on …, and prod has changed since” | The release waiting for approval was prepared against an older production. Approving it would put that older configuration back over the newer one — which is exactly how a four-day-old approval once rolled a client’s pages and schema back in the studio. | Reject it, and publish again from what you have now. |
| I pressed Deploy and nothing happened | A deploy is a job: the request enqueues it and the screen follows it, so a deploy in flight and a failed one are both reported on the screen. | If a deploy ever ends without a message, that is a defect — send the engagement and the time. |
| The deploy is taking minutes | Expected. Deploy starts a job and returns; the app is built, uploaded and started in the background. | Leave the page if you like. Run ▸ Releases carries the outcome. |
| The deploy refuses, saying the image cannot be pulled | The runtime image is private and the studio server has no registry credential, so Railway would pull it anonymously and fail. The refusal is deliberate: it happens before any infrastructure exists. | Set GHCR_TOKEN on the studio server to a GitHub token with the read:packages scope. |
| The app deployed, but a table in it is empty | The records were held back: a deploy carries about 384 KB compressed, configuration first, and records that do not fit are not sent. The finished deploy says so under Deployed, with the count and the size, and before the deploy What’s shipping estimates it (”… records held back”). Load them in the app from its Import page, as a seat that may add them. Records are loaded on the first install only — a later deploy never adds them to an app that has data. | |
| The deploy refuses, naming a role | A non-admin role can read nothing: entities grant no access until you say so, so this is what an app ships as by default. The client’s own staff would open it to empty widgets. | Build ▸ Permissions — grant that role read on the entities it needs. If nobody will ever hold that role, press “Deploy anyway — nobody will hold …” on the refusal and it deploys. |
| I suspended a firm but their app is still running | Suspension blocks the studio — sign-in, editing, deploying. The already-delivered app keeps serving its users, deliberately, so that suspending a firm does not take their client’s business offline. | If you need the app down too, undeploy it. |
| A recurring invoice did not go out | The schedule records its own failures. A period it could not bill is RELEASED rather than skipped, so the next sweep retries it — but it does not retry silently. | Open the schedule and read its last error. An archived or deleted engagement is the usual cause. |
| I cannot anchor a recurring charge on the 31st | Cron does not fire on a day a month does not have, so a 31st anchor would silently skip February, April, June, September and November. It is refused at creation rather than quietly rewritten. | Anchor on a day from 1 to 28. There is no last-day-of-month option today. |
| I added a delivered link but the client cannot see it | A delivered link needs BOTH a name and an https URL. Half-filled, the card is hidden rather than shown broken. | Fill in both. The form says so before you save. |
The screen is showing the wrong thing
| What you see | What is happening | What to do |
|---|---|---|
| A chart with axes and no bars or line, on a freshly loaded page | The axis is drawn from your real numbers, so the data arrived: the chart was painted before its panel had finished sizing. A chart repaints when its panel settles, so it fills in on its own. ⚠️ Worth knowing about, because a correctly labelled axis makes an empty chart look deliberate: it reads as “nothing sold anywhere” rather than as a fault. | It should fill itself in. If a chart stays empty with its axes drawn, switch pages and back or resize the window, and report it — an axis drawn from real numbers over no bars is a fault, not an empty dataset. |
| A chart that is a flat line, or one solid block | Charts group by the field’s exact value. One record per day means every bucket is 1, which draws a flat line correctly. For a date, group by a coarser bucket — month or quarter — rather than the exact value to date charts. | Group the chart by a date bucket — day, week, month, quarter or year — instead of by the raw timestamp. Otherwise check you have more than one record per bucket. |
| A month total that is not the month you expected | A date bucket belongs to a timezone, because an invoice at 23:30 on the 31st is next month in London and this month in New York. The caption under the chart says which zone the totals were computed in. | If the zone is not the firm’s, change it on the chart’s grouping. There is no default — whoever built the chart chose it. |
| A time on a page is hours out, or a feed files an event under the day before | A page reads times on the app’s declared reporting timezone, not the reader’s computer clock, and names the zone when the reader’s own is different (“7:05 AM BST”). A date with no time is never moved. | Set the zone in Build ▸ Settings ▸ Reporting. A care visit at 07:05 in Devon then reads 07:05 in New York too. |
| A form in Preview will not submit: “validation_failed — … Required” | The form is missing a field the record requires — a form laid out from an entity carries the fields you chose, not every required one. | Add the named fields in the form’s field list. Preview submits once every required field is on the form. |
| A chart total that is less than the table below it | Records whose date field is empty are not a time period, so they are left off the axis rather than piled into a bucket of their own. The caption says how many. | Fill in the missing dates, or accept the gap — the caption is there so the difference is never silent. |
| A chart draws several lines when its title names one — and the one you wanted is flat on the floor | The chart plots everything its data source returns. A measure that divides one figure by another returns the parts as well as the result, so a per-customer figure of £40,000 gets drawn on the same axis as revenue of £1.2M and is squashed flat. ⚠️ Nothing is wrong with the number; it is unreadable, which is worse, because a flat line reads as “nothing happened”. | Name the series the chart is about in Series on the chart’s config, and it plots that one and nothing else. Leaving Series empty plots everything, as before. |
| A chart replaced by “This widget couldn’t be displayed” | One cause is a Series entry naming something the data does not contain — usually a measure that has since been renamed. The chart is refused rather than drawn with a line quietly missing, because a chart missing a line looks finished. | The specific name it could not find, and the list of names that ARE available, are in the browser console. Fix the spelling in Series, or clear it to plot everything. ⚠️ A client viewing a deployed app cannot do this — it needs whoever authored the page. |
| A chart axis shows a plain number where I expected a percent | An axis takes a unit when every plotted series is a calculation of that unit. A mix of a rate and a count keeps a plain axis, because one scale cannot be both. | Plot the rate on its own, or give the second series its own axis. |
| An icon I set on a KPI or a button does not appear | Icons are keywords from one shared set, and a keyword the set does not have draws nothing rather than a placeholder dot — a stray dot beside a figure reads as a bullet nobody authored. | Pick from the icon list rather than typing a name. The same list draws the nav rail, the Appearance preview and the deployed app, so what you pick is what ships. |
| A markdown table renders as rows of pipes | A table needs its rule row — | --- | --- | under the header. Without it a line of pipes is treated as a sentence, deliberately: prose that happens to contain a pipe must stay prose. | Add the rule row. Alignment is read from it too (:--- left, :---: centre, ---: right), and | inside a cell needs escaping. |
| A paragraph is narrower than its card | Intended. Prose is capped at a readable measure of about 72 characters a line; a full-width card would give roughly 250, about three times what anyone reads comfortably. | Nothing to do. Tables are exempt and scroll inside themselves; for something that is not prose, use the widget built for it. |
| A prose tile is a different height than I set | Intended. A markdown tile is MEASURED and its row grows or shrinks to fit its text, so the height you author is only the first paint. | Nothing to do. Write the text; the tile takes the height it needs. |
| Publish, capture or Deploy refused: ”… can’t be published: it is marked as an argument page, and …” | The page is marked as an argument, and it is not one yet — no key line, no lead exhibit, an exhibit that supports nothing, more than five exhibits, or two takeaways. It is refused at every step that could ship it, not only at Publish, because a deploy carries drafts too. | Do what the message says: it names the page and the one thing to fix first, and says how many more there are. If the page is not meant to argue anything — a queue, a log, a form — it should not be marked as an argument at all. |
| An exported page runs to two or three sheets, or is laid out differently from the screen | Intended. Every sheet is a whole thought. A page breaks only where one of its reasons starts, and a reason that will not fit what is left of a sheet goes whole to the next one rather than being split. Nothing is shrunk to save a sheet — a chart may be drawn shorter so a reason fits, the type never. A page whose reasons sit BESIDE its lead exhibit (lead and aside) cannot be broken at a reason, because the lead crosses every row beside it, so on paper it is printed as its sequence: the lead, then each reason over its evidence. A reason whose evidence is taller than a page runs onto the next, and that page is headed with the reason (or the claim), marked continued. A page prints its lead at full size rather than scaled down, with no stray exhibit alone on the last sheet. | Nothing to do. A page that needs more than three sheets has more to say than one argument — split it. |
| A button, a form or a filter’s dropdown is missing from an exported PDF | Intended. Paper cannot press anything. Import, Export CSV, + New record, row actions, bulk selection, pagers, action buttons and forms are not printed; a paged table prints every row its box holds; a filter prints as a statement of what it is set to (Region West). If the export’s result names an exhibit “printed with a control on it”, a control slipped through — that is a defect to report, not a setting. | Nothing to do. |
| The whole page is one column | The available width is under about 768px, so it stacked: every tile full-width, one per row, in reading order. Four quarter-width tiles at that width would be 160px each. ⚠️ It measures the CONTAINER, not the window — a page embedded in a narrow panel stacks for the same reason a phone does. | Widen the window or the panel. There is no way to force the wide layout onto a narrow screen, deliberately: it was unreadable, which is why this exists. |
| A section heading appears over nothing, or a widget is missing from its chapter | A widget sits in at most ONE section. If two sections list it, it renders in the first — so it disappears from the second, and a section left with nothing is not drawn at all. | Open the widget’s Section in the inspector; it shows the one it is actually in. Re-pick to move it. |
| The canvas says “This change is not finished” and shows the page as it was | An edit is half made — most often a chart grouped by month before its timezone is chosen (a month total has to belong to somebody’s calendar, so the platform never guesses one). Until then the page does not parse, and the canvas keeps the last version that did. ⚠️ (was: “The studio hit an unexpected error”, “Reload the studio”. | Finish the edit the message names — for a bucket, choose the timezone under it. The page comes back as soon as it parses. An app that declares its calendar (App Settings ▸ How this app reports) fills it in for you, so this happens only on an app that has not declared one. |
| An annotation or callout I put on a chart is not there | If it names a point on the category axis — a month, a quarter, a stage — the name has to be the one the axis actually draws. A month bucket is stored 2026-07 and the axis prints it as 2026 07, and a coordinate that does not match a drawn label is reported rather than dropped in silence: a clean save and an empty chart. ⚠️ Found on a delivered app, where “Best month · 4.0m” had been authored, published and invisible. | The annotation is matched against the same label the axis shows, so the bucket key you authored is the right thing to type. If one is still missing, it names a category the chart’s data does not contain at all; check the spelling against the axis. |
| A filter takes up half the screen | A filter is a control, not a panel: its height is whatever it needs, which is one row. | Nothing to do — and do not author a height for it. |
An axis that loses its $ (or its %) after I put a second measure on the chart | A chart takes its currency or percent from what the plotted thing declares. A named measure declares its own unit, so give the measure the unit rather than relying on a field behind it. | Set the unit on the measure. A mix of units on one axis keeps a plain axis, because one scale cannot be both. |
A measure that is a percentage but the axis shows 0.5 | A chart takes its currency or percent from the field it plots. A measure that is a formula — a win rate, a margin, anything divided by something else — has no field, so there was nothing to take it from, and no way to say what it was: the chart had no format setting and the axis settings were read by nothing. | Set the axis label and format on the chart’s Y axis — pick percent and the axis reads 50%. The label you type there also replaces the axis title, which is the only way to name a formula measure. |
| A threshold or annotation you added to a scatter chart does not appear | The inspector offers Thresholds & Annotations on a scatter chart, but a scatter chart does not draw them. Bar, line and area charts draw both; a pie chart does not offer them, because a slice has no axis to hang a reference line on. Tooltips and axis formats are honoured on every chart type that has them. | Use a line or bar chart when the reference line matters. A setting that appears to do nothing on any other chart type is worth reporting rather than re-trying. |
| A prose card shorter than the chart beside it | Prose is measured and shrinks to its text, which is right when it stands alone; a chart has to be told its height. Where the two share a row, the row settles to its tallest member, the levelled cell is marked, and the prose card fills it — so the pair line up rather than leaving a step of dead space under the card. | Nothing: a shared row levels itself. ⚙️ A prose tile with nothing beside it still stops at its last line, which is what you want — it is only a tile sharing a row that is levelled. |
| A table, queue, board or timeline is shorter than the tile I gave it — or scrolls inside its card | Intended. These widgets hold three rows or three hundred, so the height you author is a cap, not a size: the card ends where its rows end, and past the cap the rows scroll inside the card, whole rows only. Before this they were always drawn at the authored height — Lanternhouse’s empty Open incidents queue kept a 380px card around “Nothing in the queue”. A stat grid and a form now size to their content outright, so a form is never cut off above its submit button. | Author the tallest you want the widget to get. If a table scrolls when it should not, raise the cap. |
| Money in dollars on an app that works in pounds | The app formats money in the currency and locale it declares, and an app that declares neither falls back to US formatting. | Declare the app’s currency and locale under App Settings ▸ How this app reports, then redeploy. |
An axis that reads $5m rather than 5,000,000 | Deliberate. An axis is a scale, not a table: two significant figures are what let a reader place a line, and six characters of zeros on every gridline were spending the chart width on nothing. | Nothing to do. The exact figure is on the hover, and the table below the chart still carries it in full. |
| A chart of hourly buckets whose x-axis repeats the same date on every label | An hour bucket is stored as 2026-09-04 06. Printed whole on a chart of one day — which is most of them, because a scale house or a shift works between dawn and dusk — that is the same nine characters on every label, so they collide and the hour, the only part that differs, is the part squeezed out. | Nothing: the axis shortens itself. On bar, area and line charts alike a single-day hourly axis reads 06:00, and across a date boundary it reads 09-04 23:00, because there the date is what tells two 06:00s apart. |
| A map that is dots on an empty background, with nothing to tell them apart | The map is deliberately schematic — there is no street basemap, so points are placed relative to each other rather than on a country. it also ignored the Colour by setting and stretched the picture to the panel’s shape, which made distances wrong. | Set Colour by on the map to the field that separates the points — a region, a status, an owner — and a key appears under it. If you need a real basemap, that is a different widget and not built yet. |
| A filter that does not narrow the table | A filter reaches the widgets ticked under Filters these — with none ticked, every widget whose data has its field — and leaves the others alone. ⚠️ A filter with widgets ticked reaches exactly those, and one with none ticked emptied any widget without the field. | Check the table is ticked (or nothing is), that its data has the field — the filter’s Filter on names any widget it leaves alone — and that the value exists in its data. Try it in Preview first: it filters as the app does. |
| A filter on the canvas says “Choose the field this filters on” | It has no field yet — a new filter starts that way rather than guessing one. The app draws nothing in its place. | Pick one under Filter on. |
| I need the new app’s first admin password | The studio keeps nobody’s password, so there is nothing to show. The one the deploy made was used once, to create the administrator, and then removed. | Build ▸ Deploy ▸ Sign-ins ▸ Set a new password. It is shown once — save it then. Resetting is an administrator’s action; a build or billing seat sees a sentence saying so. |
| I need a seat’s password | A seat’s generated password is shown once, when the seat is created, and kept nowhere. | Build ▸ Deploy ▸ Sign-ins ▸ the seat’s row ▸ Reset password (an administrator’s action). The new one is shown once, and the person is signed out everywhere. A seat not yet deployed is reset in Build and ships with the next deploy. |
| A secret environment variable shows only dots | By design: a stored secret is write-only — the studio never shows its value again. | To change it, ✎ Replace on its row and type the new value, then Apply. ↩ cancels and keeps the stored one. |
| A vendor needs the address and secret to send a feed into the app | The app’s own screen holds them, not the studio’s: each app generates its feed secret on its first start. | In the app: Integrations ▸ What sends to this app ▸ Sender details (an administrator’s seat). The reveal is audited. |
| A count that is one behind | A list that derives a count from another list can lag until both refresh. | Reload. If it is still wrong after a reload, that is a bug — report it. |
| The pill says “Not saved — the server refused this page” | The page has something on it the server cannot read yet — most often a row you started and have not finished, like a computed field with no name. The server checks a page before storing it, and will not store one it cannot read. | Read the sentence after the colon: it names the exact field. Finish or remove it, and your next edit saves everything at once. Nothing you typed while it was red is lost — your browser held it the whole time. |
| Dates that run backwards | An engagement whose target end is before its start is now refused at save. Older engagements created before that check may still hold one. | Open the engagement and correct the dates. |
It will not let me
| What you see | What is happening | What to do |
|---|---|---|
| “Can’t delete — records still reference this” | Other records point at this one through a reference field. The message names them and counts them. The refusal is intentional; deleting would leave those records pointing at nothing. | Open the named records, repoint or clear the reference, then delete. |
| AI stopped working partway through the month | One of two monthly limits is reached, and the message says which: your plan’s token allowance, or your workspace’s AI spending cap — the studio’s AI assists and its other AI features share that cap. Both reset at the start of the next month. | Wait for the reset, or move up a plan. |
| A person in a client’s app has no assistant, though others do | Asking the app’s assistant is a permission, Use the assistant, because every question spends the app’s AI allowance. A role without it is never shown the assistant. The app’s administrator role holds it already. | Build ▸ Permissions ▸ the person’s role ▸ Grant app permissions ▸ Use the assistant, then deploy. |
| I cannot invite another consultant | Your plan’s seat limit. Active seats only — a revoked member does not count against you, and a pending invitation is not a seat until it is accepted. | Revoke a seat you are not using, or move up a plan. |
| An invitation was refused when the person accepted it | The seat was still free when you sent it and gone by the time they opened it — acceptance is where the seat is actually taken. They see a message telling them to ask you. | Free a seat or move up a plan, then have them open the same invitation again. |
| I cannot reactivate a member I suspended | Reactivating takes a seat back, so it is refused when the plan is full — the same refusal as an invite. | Revoke someone else first, or move up a plan. Seat usage is on Billing beside the app allowance. |
| An invite was refused with “cannot build an absolute invite link” | This server has no origin configured for the accept link, so the invitation was not sent. It refuses rather than emailing a link nobody can click | Infrastructure, not your firm: OPERATOR_APP_BASE_URL must be set to the studio origin. Tell whoever administers this server. |
| I cannot deploy another app | Your plan’s app limit, counted per app, not per deploy. Redeploying an existing app is always allowed. Live apps only — a retired app does not count against you, and neither does a preview site or a Local Docker deploy. Build ▸ Deploy says how many of your plan’s apps are in use above the Deploy button, before you press it. | Retire an app, or move up a plan. |
| A seat cannot find a page everyone else talks about | The page is limited to seats with a given attribute or role — the page editor’s Visible to seats whose… and Visible to roles. To any other seat it does not exist: not in the menu, not openable, not exportable; opened from a link, the app says “This page isn’t available”. The limit binds seats whose rows the attribute scopes — a regional seat with no market set sees none of those pages (set the value under Build ▸ Permissions ▸ Users), while a seat that reads every market sees them all. Build ▸ Permissions ▸ Test as user lists exactly the pages each seat gets. | |
| “Publish to Production” is refused as stale right after “Published to staging” | A publish writes the page, so the version you are holding is one behind the one the server now has. | Reload the page and publish again. |
| “Editing conflict — another editor saved this page” right after you published | Publishing writes the page and brings its saved draft onto it; the editor kept the draft’s old revision, so its next autosave looked like a collision with someone else. Nobody else was there. | On an older studio, choose Reload — your published work is already the page. A conflict that names another person is real: read it before forcing a save. |
| The Publish button is off the right edge | with a page open, the editor’s controls — Publish among them — have their own row under the top bar, instead of pushing the bar past the window on a laptop screen. | — |
| The studio says “Not found” or “Not available to your seat” | An answer, not a failure: the thing does not exist (a stale link, a deleted record) or your seat may not open it (platform administration is the platform operator’s). There is nothing to retry. It reads the same way whether it is a PAGE you opened or an ACTION you took — a refused button names the authority your seat is missing, rather than suggesting something broke. | |
| Preview says the template cannot be instantiated | One or more template field roles are unmapped. The message lists each one. | Map every role. If a role has nothing sensible to map to, your entity is probably missing a field. |
I cannot find the control
| What you are looking for | Where it is |
|---|---|
| Options for a dropdown field | Behind more on the field’s row, not on the row itself. |
| Records, when the button is greyed out | Save the entity first — the button says so. Records cannot exist before the shape they belong to. |
| Which version is live | Run ▸ Releases. |
| Who changed something | Run ▸ Audit, scoped to the engagement. |
| Why a notification went nowhere | The rule’s recipient. Free text that resolves to nobody, or a role with no members, both send to nobody. |
Notifications and rules
| What you see | Usually |
|---|---|
| The rule did not fire | It is scoped to a different entity than the record you changed, or it was never published. A rule scoped to one entity is silent on every other. |
| The notification went to everyone | The rule has no entity scope. An unscoped rule fires on every entity, including ones it never names. |
| The notification reached the bell, but no email came | Email is off until each person turns it on — you and your team as well as your client’s staff, and that includes a question your client sends from the portal, which reaches you in the bell. Turn it on from your avatar ▸ Notification preferences ▸ Email; in a client’s app, each person does it from the bell ▸ Preferences. Sign-in links, invitations and invoices are not affected: they always send. |
| The notification went to nobody | The recipient resolves to no one — a role nobody holds, or free text matching no user. The rule still fires; it just reaches nobody, and nothing warns you. |
| The watcher never fires | An enabled watcher with no anomaly_detected notify rule for its entity detects anomalies but reaches no one. Publishing warns you — the message names the entity and tells you what to add. Also check sustain: it requires that many consecutive windows to breach. |
| The import duplicated everything | The pipeline has no de-duplication key, so every run re-inserts the whole source. ⚙️ Only possible on manual runs — a scheduled import without a key is refused outright. |
If nothing at all happened
A write that fails must say so. A rejected save that closes the dialog as though it worked is a defect, not a quirk.
So if you click something and nothing happens — no confirmation, no error, no change — that is worth reporting. It is not you.
Report it with the three things that make it actionable:
- What you clicked — the screen and the control, not “it broke”.
- What you expected, and what happened — including “nothing happened”.
- Whether the server saw it — Run ▸ Audit. A click that never reached the server is a different problem from one the server rejected.
The habits that prevent most of this list
- Publish and deploy deliberately, and know which version is live.
- Scope every rule to an entity.
- Preview every watcher before saving it.
- Test every notification from both sides: the right role receives it, other roles receive nothing.
- Give every import pipeline a de-duplication key.
- Sign in as a real role to check permissions. Do not trust the matrix.
- Take a backup before anything you would not want to redo.