Runs, queries and deploys lists
The three lists that show what happened in an environment: the filters of each, the order, the cursor that pages through it, who sees what, the commands, the SDK and the routes. For the steps, see Find a run, a query or a deploy.
The lists
| List | Command | Needs | Order |
|---|---|---|---|
| Runs | lakana runs list |
READ_ENVIRONMENT |
creation time, newest first; equal times by id |
| Queries | lakana query list |
SUBMIT_QUERY; MANAGE_ENVIRONMENT shows everyone's |
running, then queued, then (with --all, or an ended status) ended ones |
| Deploys | lakana deploys list |
READ_ENVIRONMENT |
creation time, newest first; equal times by id |
Every filter narrows the list; filters combine with AND. A filter is never a way to see more than the list shows without it.
Filters
| Filter | Option and parameter | Values | Lists |
|---|---|---|---|
| Repository | --repo, repository |
a repository name; an unknown name is 404 |
runs, deploys |
| Status | --status, status |
runs: queued, running, succeeded, failed, cancelled. Queries: the same words in any letter case (the list shows them in capitals). Repeat the option or separate the values with commas: failed,cancelled |
runs, queries |
| Trigger | --trigger, trigger |
schedule, api, manual; repeat or separate with commas. A run started with lakana run has none and matches no trigger |
runs |
| Job | --job, job |
the name of a job | runs |
| Since | --since, since |
an ISO 8601 time with a time zone, for example 2026-10-18T09:30:00Z; the run was created at or after it |
runs |
| Submitter | --submitter, submitter |
a principal's name; none of that name's queries is shown to someone who may not see them | queries |
--since on the command line also takes a date (2026-10-18) or a time without a time zone, which
is read in the time zone of your computer. The route needs the time zone: since=2026-10-18T09:30:00
is refused with 422.
A query status that has ended (succeeded, failed, cancelled) lists the queries that ended in
the last 24 hours, with or without --all. The list never shows older ones.
Paging
--limit (limit) is the most rows per page: 1 to 200, 100 by default. When more rows follow, the
route answers with the header X-Next-Cursor, and the command prints the line that shows the next
page:
More results: lakana runs list --env dev --status failed --limit 50 --cursor eyJrIjogInJ1bnMiLCAibyI6ICIyMDI2LTEwLTE4VDA5OjMwOjEyLjM0NTAwMCswMDowMCIsICJpIjogIjVjMGI3ZDRlLTJmMWEtNGI4ZS05ZDYzLTBhMWU3YzRmOWIyMSJ9
With --output json the line goes to stderr, so the JSON on stdout stays one list. The cursor is
opaque: pass it back unchanged, with the same filters and the same limit.
A page continues strictly after the last row of the previous one. A run, query or deploy that
arrives while you page is never shown twice, and none that was there is skipped. The one exception
is a query that changes from queued to running between two pages, which moves to another part of
the order of queries. The cursor belongs to its list: the cursor of the runs list is refused by the
queries list, with 422 and a message that names both lists. Start again without cursor.
Refusals
| Cause | Answer |
|---|---|
a status, trigger or limit that is not allowed |
422; the message names the parameter and the allowed values |
since without a time zone, or not a time |
422; the message shows how to write it |
| a cursor that was not issued, or came from another list | 422 |
| an unknown repository | 404 |
| a caller who may not read the environment | 403 |
| a manager's list that shows other people's queries, or a page of the audit log that shows other people's events, and could not be recorded in the audit log (see Audit) | 503 with Retry-After; nothing is listed, and the message names the cause |
Audit
A list of runs or deploys is not audited, and neither is a list of your own queries. When a manager's
query list shows someone else's query, or filters on someone else with --submitter, the audit log gets
one ACTIVITY_VIEWED event, and so does seeing the readers of a table
(table health) and reading the audit log itself (lakana audit list) when
the page shows someone else's events or filters on someone else with --principal-id:
| Field | Value |
|---|---|
| principal | who looked |
| object | the environment of the query list, the table whose readers were shown, or the instance for the audit log |
detail.view |
queries, table_readers or audit_log |
detail.filters |
the filters of the first look: include_ended, status and limit for queries, days for readers, event_type, environment, since, until and limit for the audit log |
detail.rows_shown |
how many rows the answer showed |
detail.others_shown |
how many of them were other people's (a deleted principal's, and an event of nobody, such as a failed sign-in, included) |
detail.distinct_others |
how many other people those rows belong to |
detail.targeting_key |
{"submitter": "NAME"} when the query list was filtered on someone else, {"principal_id": "ID"} when the audit log was, else null |
The event never holds the names of the people seen, the ids of their queries or any SQL. The same
person, view, environment or table and targeting key write one event per window: the next page,
another status or event type, --all and a screen that refreshes itself add nothing. Looking only at
your own queries or your own events, filtering on yourself, and a task's token record nothing. An
instance administrator is recorded like anyone else.
If the event cannot be written, for example because the database is full, the list is refused with
503 and shows nothing; try again in a moment, and if it keeps failing ask the operator to check the
server's database. Read the events with:
lakana audit list --event-type ACTIVITY_VIEWED
| Setting | Default | Meaning |
|---|---|---|
LAKANA_VIEW_AUDIT__ENABLED |
true |
Record the event; false turns it off. |
LAKANA_VIEW_AUDIT__WINDOW_MINUTES |
60 |
Minutes during which the same view by the same person writes no second event (1 to 1,440). |
Other people's query text and column values in the audit log
Two fields of an event can hold content, which may be anything a table holds:
| Event | Field | What it holds |
|---|---|---|
QUERY_COMPLETED |
detail.sql_text |
the SQL text of the query |
TABLE_WRITE |
detail.watermark |
the highest value of an incremental write's key column, a value from the table, such as date:2026-10-20 or long:7340219 |
Reading the audit log needs READ_AUDIT_LOG; seeing these fields in someone else's events
needs READ_QUERY_TEXT as well, which no role carries, ADMIN included. READ_QUERY_TEXT covers
query text and column values in audit events, nothing else. Without it:
- Query text shows its SHA-256 and its length in characters, so that the same query can still be recognised across events. The hash is not a secret: someone who guesses the exact text can confirm the guess, which is easy for a short or common query.
- A watermark shows only
{"hidden": true}: no hash and no length. Its values are numbers, dates and times, few enough to try them all against a hash, so a hash would give the value away.
"sql_text": {"sha256": "9f1c...e2", "length": 57}
"watermark": {"hidden": true}
Your own events always show in full. Holding READ_TABLE on the table does not show a watermark
either: the right is about the audit log. A field of an event that the server does not know how to
classify shows {"hidden": true} too. An administrator gives the right to a person with
lakana grant create --kind INSTANCE --principal NAME --action READ_QUERY_TEXT; the grant and its
revoke are in the audit log like every grant, and lakana access explain NAME lists it. It is given
on the instance only.
How much text a new event stores is set on the server and every worker:
| Setting | Default | Meaning |
|---|---|---|
LAKANA_AUDIT__QUERY_TEXT |
full |
full stores the text; hash only its SHA-256 and length, so the text itself is not kept; off neither (null). Events already written keep what they stored. A query's own record keeps its text for its submitter either way. |
SDK and REST
| What | SDK | Route |
|---|---|---|
| Runs, one page of runs | client.runs.list(env, ...); client.runs.list_page(env, ...) also returns the cursor |
GET /api/v1/environments/{env}/runs?repository=&status=&trigger=&job=&since=&limit=&cursor= |
| Every run that matches | client.runs.iter(env, ...) |
the same route, page by page |
| Queries | client.queries.list(env, ...); client.queries.list_page(env, ...) |
GET /api/v1/environments/{env}/queries?include_ended=&status=&submitter=&limit=&cursor= |
| Deploys | client.deploys.list(env, ...); client.deploys.list_page(env, ...) |
GET /api/v1/environments/{env}/deploys?repository=&limit=&cursor= |
The routes answer with a JSON list, as they did before they could be paged, and the cursor of the
next page is in the X-Next-Cursor header (absent on the last page). list returns the rows of
one page; list_page returns a page with items and next_cursor. The SDK takes a status or
trigger as one word or a list of words, and since as a datetime with a time zone.
from datetime import UTC, datetime
import lakana
client = lakana.Client()
page = client.runs.list_page(
"dev", status=["failed", "cancelled"], since=datetime(2026, 10, 18, tzinfo=UTC), limit=50
)
for run in page.items:
print(run.id, run.status, run.job)
if page.next_cursor is not None:
more = client.runs.list_page(
"dev",
status=["failed", "cancelled"],
since=datetime(2026, 10, 18, tzinfo=UTC),
limit=50,
cursor=page.next_cursor,
)