Open SQLly on a query

Launch SQLly straight into a query tab: the script, its connection and database, parameter values and the cursor, sent with the launch token for "Open in SQLly" buttons.

A launch token normally opens an empty workbench. Add an open field when you mint it and SQLly opens straight into one or more query tabs instead: the script loaded, the tab connected to the right server and database, your parameter values filled in, the cursor where you put it. That gives you "Open in SQLly" buttons anywhere your app shows SQL: next to a report, on a failed query, in an audit trail.

Nothing runs on its own. A launch only loads tabs. The person in the browser reviews each query and presses Run, the same rule SQLly's desktop query links follow. There is no way to make a launch execute SQL.

Mint a token with launch context

It is the same POST /manage/tokens call as any launch (step 4 of building your own app), with one extra field:

shell
curl -s -X POST -H "Authorization: Bearer $SQLLY_EMBEDDED_MANAGE_TOKEN" \
  -H 'content-type: application/json' http://127.0.0.1:8445/manage/tokens -d '{
  "user": "dana.analyst",
  "profile": "analysts",
  "open": {
    "connection": "Shop",
    "database": "sales",
    "title": "Big orders",
    "script": "SELECT *\nFROM orders\nWHERE total > @min_total AND placed_at >= @since",
    "parameters": { "min_total": 500, "since": { "type": "date", "value": "2026-10-01" } },
    "cursor": { "line": 3, "column": 7 }
  }
}'

The reply is the usual token plus openTabs, the number of tabs the session will open:

{ "token": "…", "expiresAt": "2026-10-18T15:12:11Z", "proxyUrl": "wss://portal.example.com/sqlly/proxy",
  "user": "dana.analyst", "profile": "analysts", "seatsUsed": 3, "seats": 25, "openTabs": 1 }

Hand the token to the page exactly as for a plain launch. When the user arrives, SQLly connects to Shop, switches to sales, and shows a tab called "Big orders" with the query loaded, the parameter strip holding 500 and 2026-10-01, and the caret on the third line.

What a tab can carry

open is one object, or a list of up to 8 for several tabs. The first one gets the focus. Every field is optional, but an entry needs at least a script or a connection.

FieldTypeMeaning
connectionstringA connection in the token's profile, by name (any letter case) or by its id. Leave it out when the profile has only one connection. With several and none named, the tab opens without one.
databasestringThe database to switch to instead of the connection's default. At most 128 characters.
titlestringThe tab's title. At most 120 characters. Without it, SQLly names the tab as usual.
scriptstringThe SQL to load, verbatim. At most 256 KiB. Windows line endings are folded to \n.
parametersobjectValues for the script's @name parameters, at most 64. See parameters.
cursor{ line, column }Where the caret goes, both counted from 1, the column in characters. It must point inside the script (a column one past the end of a line is allowed).

Parameters

SQLly has typed query parameters: a -- sqlly param: @name <type> line declares one, and a strip above the results holds its value. Launch parameters use that same strip, so your values show up as visible, editable inputs and the statements themselves are loaded exactly as you sent them.

  • Names use @name, on every engine. Send the key with or without the @ ("min_total" and "@min_total" are the same). A name is a letter or _ followed by letters, digits or _, at most 64 characters, and may appear only once (any letter case). If your app writes parameters another way ($name, :name, ?), rewrite them to @name when you build the script.
  • A plain value picks its type from JSON: a whole number is int, any other number decimal, true/false bool, a string text, and null leaves the input empty for the user to fill in.
  • A typed value is { "type": "date", "value": "2026-10-01" }. The types are text, int, decimal, bool, date, datetime, time, uuid, json and binary. Send JSON documents as strings with "type": "json".
  • Declarations are added for you. For each parameter the script doesn't declare, SQLly adds a -- sqlly param: line above the script, and the cursor moves down with the script. A parameter the script already declares keeps the script's declaration and type, and gets your value.
  • How values reach the database. When the user presses Run, SQLly checks each value against its type and replaces each @name outside strings and comments with a literal written correctly for that engine, then sends the statement. A value that doesn't fit its type stops the run and focuses that input. Your app never splices values into the SQL text.

One launch, one token

  • The tabs open once. The bridge hands them to the first SQLly session that uses the token and then forgets them. Reloading the page restores the workbench, which SQLly saves in the browser, so the tabs aren't opened twice. The token itself still works until it expires.
  • Mint a fresh token for every "Open in SQLly" click. Don't reuse a cached token for a launch with context: it would open nothing. A new token for a user who already holds a seat costs no seat.
  • Each click is a new tab. It counts against the user's sessionsPerSeat (four on paid plans, one on the trial). On the trial, the user has to close their other SQLly tab first, or the new one is refused with "<user> already has the maximum number of SQLly sessions open".
  • The context is fixed at mint time, like the profile. Connections are resolved against the profile as it is then.

Mistakes are caught at mint time

The bridge checks the whole open value before it creates the token, so your server finds out right away rather than a user finding a broken tab. A bad value is 400 invalid_open with one entry in problems per mistake, each naming where it is. No token is minted and no seat is taken:

{
  "error": "invalid_open",
  "message": "the launch context has 3 problems",
  "problems": [
    "open[0].connection: no connection \"Shopp\" in this profile (it has: Shop, Warehouse)",
    "open[0]: unknown field \"sql\" (allowed: connection, database, title, script, parameters, cursor)",
    "open[1].cursor: line 9 is past the end of the script (3 lines)"
  ]
}

Unknown fields are refused rather than ignored, so a typo like sql for script fails loudly. Values of type int, decimal and bool are checked too. Other types are checked by SQLly's parameter strip before a query runs.

Recipes

Reopen a query from the audit trail

The query audit trail records each query with its profile, connection, database and SQL, which is everything a launch needs. The sample app's Queries page has a Reopen in SQLly button on every complete statement. It looks the row up on the bridge again (the form names the row and nothing else), then mints an admin launch with this context:

javascript
const entry = (await bridge('GET', '/manage/queries?limit=1000')).queries
  .find((q) => String(q.sessionId) === form.session && q.id === form.id);
if (!entry || entry.kind === 'row-edit' || entry.sqlTruncated) return refuse();

const open = {
  connection: entry.connection,
  title: `${entry.status === 'failed' ? 'Failed' : 'Query'} from ${entry.user}`,
  script: entry.sql,
};
if (entry.database) open.database = entry.database;
const minted = await bridge('POST', '/manage/tokens', { user: `admin:${admin.id}`, profile: entry.profile, open });

The logged SQL is what actually ran, so parameter values are already literals in it and no parameters are needed. For a failed statement the sample app also reads where the engine says it failed (PostgreSQL's POSITION: 53, MySQL's "at line 2") and sends that as the cursor, so the caret lands on the offending column.

"Open in SQLly" on your app's error page

If your app runs SQL of its own (a report builder, a SQL-driven page framework, scheduled jobs), it knows at the moment a statement fails which statement it was, the values bound to it and which database it ran against. Put a button on the error page that mints a launch with that statement, its values as parameters, and the cursor on the line the database complained about. The developer gets a full workbench on that query, with IntelliSense, the object explorer and execution plans, without copying anything by hand:

javascript
// failure = { sql: 'SELECT … WHERE id = $id', params: { id: 42 }, line: 3 }
const script = failure.sql.replace(/\$([A-Za-z_]\w*)/g, '@$1');   // $id → @id
const minted = await bridge('POST', '/manage/tokens', {
  user: req.user.id,
  profile: 'app-database',            // a profile pointing at the app's own database
  open: {
    title: 'Failed statement',
    script,
    parameters: failure.params,
    cursor: failure.line ? { line: failure.line, column: 1 } : undefined,
  },
});

Only offer this to people who should have a workbench on that database, for example in a development environment or for administrators. The profile decides what they can reach, exactly as for any other launch.