ZuloOne
English
Sign in
Menu

The command run journal

Updated

Every call of a command leaves one row in MetaCommandRuns: who ran it, on what, what went in, what came out, how long it took, and — when it went wrong — the exception. The journal is what the Command statistics screen drills into.

← Developer wiki

Why the counter was not enough#

Each of the five command-family tables carries ExecutionCount and LastExecutedUtc. Both are written after a successful run. That makes the counter useless for the question people actually ask:

Question The counter The journal
How often did this command run? successes only every call
Did it ever fail? invisible Success = false + ErrorDetails
Why did it fail? — the exception, as thrown
What was it called with? — ArgsJson
What did it answer? — ResultJson
Did the user ever finish the dialog? — AwaitingAnswer = true
How slow is it? — DurationMs

A command that throws on every call sits at ExecutionCount = 0 forever, next to a command that was never wired to a button. The journal tells them apart: Calls 14 / Errors 14 versus Calls 0.

Worked example#

A document command PostSalesInvoice is pressed four times on the stand:

# What the user did Outcome
1 Pressed it on invoice SI-0007 asked "Post with a negative stock?"
2 Answered Yes posted, Draft → Posted, 412 ms
3 Pressed it on SI-0008, which has no lines refused: "Document has no lines"
4 Pressed it on SI-0009 threw: NullReferenceException in the tx script

Runs still counts successes only. The row also shows every call, and opens its four runs:

Command Runs Calls Errors Average Last run
PostSalesInvoice 1 4 2 173 ms 14:31
Started Status User Object Time Message
14:31 Error ops SalesInvoice 38 ms Object reference not set…
14:27 Error ops SalesInvoice 11 ms Document has no lines
14:22 Done ops SalesInvoice 412 ms Posted
14:21 Question ops SalesInvoice 230 ms —

Opening run #4 shows the input ({"parameters":{"Force":false}}), the output, and the full stack trace. Opening run #2 additionally shows Subtype: Draft → Posted.

What is stored#

ZuloOne.Metadata.MetaCommandRun — one row per invocation:

Column Meaning
Kind user / dictionary / dictionarylist / document / documentlist, or legacy for the old flat pipeline
CommandMetaId, CommandName, CommandCaption the command; the name is denormalized so a deleted command still reads
UserMetaId, UserName who ran it; the name is denormalized, so deleting the user does not erase the history
TargetObjectType, TargetObjectMetaId, TargetObjectName the dictionary / document type it ran against
RecordId, SelectionCount the single record, or how many were selected on a list command
StartSubtype, EndSubtype document subtype values before and after — equal when the command did not move the document
ArgsJson the input: {"parameters":{…},"answers":{…}}
ResultJson the output: data, validationErrors, clientActions, redirectUrl, shouldReload
StartedUtc, DurationMs when and how long
Success, AwaitingAnswer the outcome; AwaitingAnswer means the run stopped on a question, which is not a failure
Message, ErrorDetails the message shown to the user and the exception as thrown

Two indexes: (CommandMetaId, StartedUtc) for the drill-down and StartedUtc for the "latest runs of anything" feed.

What is NOT stored#

Secret-looking parameters. A value whose key contains password, passwd, pwd, secret, token, apikey, api_key, privatekey, private_key, credential, pin, otp or cvv (case-insensitive substring) is replaced with *** — in ArgsJson and in the Args of every client action. The key is kept: it matters that the command was called with a password, it does not matter which one.

Oversized payloads. ArgsJson and ResultJson are capped at 8000 characters; a longer payload is cut and ends with …[truncated]. A command that returns a 10 MB table must not turn the journal into a second copy of the database.

Anything older than 90 days. A set-based delete runs at most once every six hours, piggybacking on the next recorded run. The journal is operational history, not an audit trail — "who changed what" stays in the audit log.

All three limits live in CommandRunPayload (ZuloOne.Core/Services), which has no EF dependency and is pinned by CommandRunPayloadTests.

How it is written#

CommandRunJournal.RecordAsync writes from its own DbContext, in a finally, with CancellationToken.None. Three consequences, all intentional:

  • a command whose own work was rolled back still leaves a run — that is exactly the run worth reading;
  • an aborted request still leaves a run;
  • a journal that cannot write never fails the command — it logs a warning and returns.

Both pipelines write through it: CommandFamilyService.ExecuteAsync (api/commands2) and CommandService.ExecuteCommandAsync (the legacy api/commands). The early refusals are journalled too — "Command is disabled", "recordId is required", "No records selected", "Parameter 'X' is required" — because those are precisely the runs that would vanish without trace.

The input is serialized before the handler runs: a script is free to mutate its own parameter bag, and the journal must show what the caller sent.

Subtype capture is narrow on purpose#

StartSubtype is read only for a document command on a known document type with exactly one record selected, and EndSubtype only when the start was captured. A list command over 500 documents would otherwise cost 1000 extra reads. A document command with an empty subtype binding set resolves no target type, so it captures no subtype either.

API#

Endpoint Returns
GET /api/commands2/runs/summary per command: total, failed, asked, lastRunUtc, avgMs
GET /api/commands2/runs?commandMetaId=&kind=&success=&userMetaId=&take= runs newest-first, without the heavy payloads (hasArgs / hasResult / hasError instead); take is clamped to 1…500
GET /api/commands2/runs/{id} one run in full

All three are gated by the commandruns system registry (SystemObjects.ByKey("commandruns"), object type System, operation Read) — unrestricted until a permission row for it exists, like every other kernel registry.

In the UI#

Tools → Command statistics. The grid gains three columns — Calls, Errors, Average — beside the existing Runs counter. Clicking a row opens its runs in a drawer; clicking a run shows its input, its output and its error. The action pane has Latest runs, the same drawer across all commands.

Administrator → Monitor → Command run journal is the journal as its own page: every run of every command, newest first. All and Errors only switch the filter, and a click on a run opens the same detail view. It is gated by the commandruns system registry, like the API, so a role without Read on it does not see the ribbon item.

Where next#

Was this page helpful?