Skip to content

Inspecting and Debugging ​

When something in a running app needs a closer look — did that function run, what did it print before it failed, who is connected right now, what does this blob's metadata say — the CLI is the fastest way in. Its inspection commands read the same server state the Admin Console shows, but from the terminal, so they fit into scripts and agent workflows.

They share one set of conventions, so once you know how one reads, you know how they all read.

The inspection surface ​

bash
# Server functions
primitive functions list                                  # the app's functions, their status, and each one's webhook and crons
primitive functions get <function-id>                     # active version, manifest, webhook and cron triggers
primitive functions runs <function-id>                    # task runs, cron fires and webhook deliveries
primitive functions runs steps <function-id> <run-id>     # one task run, step by step
primitive functions logs <function-id>                    # what each invocation printed and threw
primitive functions logs <function-id> --run <run-id>     # one run's step trace and output
primitive functions logs <function-id> --invocation <id>  # one invocation's record

# `runs steps` works on a run that is still executing: it shows the steps
# finished so far plus a `running` row for the current one.

# The other log views
primitive integrations logs <integration-id>           # outbound calls an integration made
primitive integrations logs <integration-id> --run <run-id>  # just the calls one run made
primitive analytics events                             # app activity events

# Blob storage
primitive blob-buckets list                            # buckets in the app
primitive blob-buckets head <bucket> <key>             # object metadata without downloading

# Live connections and sessions
primitive connections list --user-id <id>              # active WebSocket connections
primitive sessions list --user-id <id>                 # auth sessions

# Records and documents
primitive databases list --owner <user-id>             # databases one user created
primitive databases records query <database> ...       # read database records
primitive databases records get <database> <model-name> <record-id>
primitive databases records count <database> <model-name> [--filter '{...}']
primitive databases records aggregate <database> <model-name> --op <count|sum|avg|min|max>
primitive documents records query <document> <model-name> [--filter '{...}']
primitive documents records get <document> <model-name> <record-id>
primitive documents records count <document> <model-name> [--filter '{...}']
primitive documents records aggregate <document> <model-name> --op <count|sum|avg|min|max>
primitive documents dump <document-id>                 # every model's records as JSON
primitive documents export <document-id>               # dump a document's contents

# Metadata
primitive metadata get <type> <id> <category>          # resource metadata

How every inspection command reads ​

The read flags are the same everywhere:

  • --app <id> targets an app, falling back to the environment's app when you omit it.
  • --json is the output you parse in a script. Most commands print the endpoint payload as-is; the log views below normalize theirs into one shared item shape. Either way it is a single JSON document, never a bare array, and it writes to stdout while status, progress, and the CLI Version: … banner go to stderr, so a redirected stdout stays one parseable document. The same holds for commands that are always JSON and take no --json flag: primitive documents dump <doc> | jq . parses.
  • Every list prints the same envelope. Whatever noun you asked about — with no exceptions, the type-config readers included — list --json prints { items, hasMore, nextCursor? }: the rows in items, a boolean hasMore telling you whether the listing continues past them, and nextCursor only when it does. jq '.items[]' therefore works against any listing, and an empty one prints { "items": [], "hasMore": false }. A list that carries something which is not a row — env list's current selection, guides list's resolved docs version — prints it as an extra key beside those three.
  • --limit <n> and --cursor <c> page through long results, and which listings have them follows the route rather than the verb. A list whose route pages takes both flags, prints exactly one page, and reports that page's hasMore and nextCursor; pass the cursor back as --cursor to fetch the next one. It never walks the chain for you, so a script that wants everything follows nextCursor until hasMore is false. A list whose route does not page takes neither flag and prints the whole set with hasMore: false. primitive help --json tells you which flags a verb carries. functions logs and both records query verbs print the same envelope, whatever shape their endpoint returns.

list always requires a selector — a --user-id, --owner, or resource id — so it never dumps the whole app at once. --user-id is the spelling everywhere. The exception to the selector rule is a resource that is genuinely app-wide, like blob-buckets list or functions list, which list the app's buckets and functions directly.

A couple of verb names differ by resource on purpose, because their permission models differ: documents grant access with permissions grant / permissions revoke (a reader / read-write / owner ladder), while databases use permissions add-manager / permissions remove-manager (a manager / owner ladder). Reading permissions is uniform — permissions list — only the mutation verbs diverge.

Reading the log views ​

The log views — a function's invocation records, integration calls, and activity events — answer different questions but read the same way under --json. Every item carries the same envelope, so one script can handle all of them:

json
{
  "source": "function-log",
  "timestamp": "2026-07-24T18:03:11.204Z",
  "outcome": "error",
  "nativeStatus": "failed",
  "correlation": { "eventId": "01J…", "runId": "01J…", "userId": "01J…" },
  "detail": { "functionKey": "summarize", "triggerKind": "cron", "runtime": "task", "errorMessage": "…" }
}

source tells you which view the item came from — function-log, integration, or activity. outcome is the normalized verdict, one of ok, error, pending, or neutral, so a scan for trouble is a filter on one field regardless of source. nativeStatus keeps the underlying value each store recorded — an HTTP status code for an integration call, failed or timeout for an invocation — for when you need the specific reason.

correlation is what makes a debugging session move between views: it carries the ids the item is linked by — the invocation's own eventId, the runId of a task run or a trigger fire, the userId it is attributed to — so a failing record leads to its run, and its run to the steps. detail holds the operator-facing fields specific to that source.

An integration item carries the same pivot keys, which is what links an outbound call back to the code that made it: correlation.runId for the run the call came from — a workflow run or a server function's task run or trigger fire — plus correlation.stepId for a workflow step, correlation.functionId for a server function, and detail.functionKey beside detail.callSource. A call made from an HTTP request invocation carries no runId, because a request invocation writes no run row.

Items always arrive inside the view's envelope, never as a bare array — { items, hasMore, nextCursor? } for functions logs, { items } for integrations logs, and { items, page, pageSize, totalRows } for analytics events. --follow --json is the one variation: it emits one item per line, because a tail has no closing bracket to wait for.

This normalization is --json-only. The human tables stay per-view, since each shows columns the shared shape has no room for.

Triaging a failing function ​

Start from the invocation records. functions logs lists them newest first, one row per invocation, with the version that ran, the error code, and the first line of the error:

bash
primitive functions logs <function-id>
TIME       STATUS     TRIGGER  RUNTIME  VERSION  RUN/INVOCATION ID  CODE  ERROR
10:02:11   failed     cron     task     01J8…    01J8…                    Database "orders" not found
09:41:07   completed  http     request  01J8…    01J8…
09:12:55   timeout    webhook  request  01J8…    01J8…

The row says what happened; --json carries the rest — the whole error message and stack, and every line the function printed to stdout and stderr, in the order it printed them. To read one record in full, pass the id an invoke answered with, or the one in the table:

bash
primitive functions logs <function-id> --invocation <invocation-id>

For a task run, read the run rather than the record. --run prints the run's step trace first, then each record it wrote, oldest first, with its output underneath:

bash
primitive functions logs <function-id> --run <run-id>
primitive functions runs steps <function-id> <run-id>   # the step trace alone

functions runs answers at the level of the run: its status, what fired it, the runtime and version it used, and its error code. A cron fire and a webhook delivery each leave a run row, so this is also where you check what a schedule or a provider actually did. The triggers themselves are on functions list, for the whole app in one call: functions list shows which functions carry a webhook or crons, with each one's status, schedule, next and last fire. functions get remains where the receiver URL, ids and secret state are read — the webhook's id, URL, scheme and signing secret, and each cron entry's id, fire count and last run.

Invocation records are kept for seven days, and they outlive the function: archiving a function leaves its records readable until they expire. See Debugging a failing function for what a record captures, its size caps, and how secrets are redacted.

Following one user ​

Activity events can be read for a specific user:

bash
primitive analytics events --user-id <user-id>      # that user's activity events

A function invocation is recorded there as a function.invoke event, and its record in functions logs carries the same user as correlation.userId.

Work with no human behind it is recorded against the app's own principal, sys:<appId>. That id has no user account, so the surfaces that show a name show System for it: the USER column of analytics events, and the Name line of primitive analytics user-detail sys:<appId>. The stored id is unchanged, so analytics events --user-id sys:<appId> reads exactly that activity.

Watching live: --follow ​

primitive functions logs --follow tails a function's invocations the way tail -f tails a file. Its first poll sets a baseline and prints nothing that predates the moment you started, then appends each invocation as its record is written:

bash
primitive functions logs <function-id> --follow                # poll every 2s
primitive functions logs <function-id> --follow --interval 5   # poll every 5s

A few rules keep it predictable:

  • --interval <seconds> sets the poll interval, 2 by default. The CLI polls; Primitive does not push these updates.
  • A tail starts from now, so it can't be combined with --cursor, and it walks every invocation, so it can't be combined with --run either.
  • Under --json, a tail emits NDJSON — one JSON object per line, each in the shared item shape — because a never-ending stream can't be a single document; pipe it to jq -c.
  • Press Ctrl-C to stop; the command exits cleanly.

--follow shows a slow invocation even when a faster one settled first. An invocation's id is minted when it starts and its record is written when it settles, so a call that began before another and finished after it lands below rows the tail has already printed. The tail therefore looks back a minute past its own high-water mark — the longest a request record can lag its id, the 30 s request ceiling plus the token's grace — and remembers which ids inside that window it has already shown you, so each record is printed once. That matters, because slow, failed and timed-out invocations are usually the ones you are tailing for.

Documentation validated against js-bao-wss-client 3.4.0 · js-bao 0.11.0 · primitive-admin 1.0.62 · primitive-app 3.1.0 — 2026-09-30