A Transaction is one unit of work. The queue hands one to processTransaction, the automation does the work and the
framework records how it ended. This page follows that path.
#What a transaction is
interface Transaction {
id: string; // stable unique id, e.g. "ORD-1001"
payload: Record<string, unknown>; // any JSON-serializable work data
}
It is plain data, never a DOM node or a page object. id is what appears in the logs, in state.json and in loop
protection, so it should identify the business item, not a row number. payload is whatever the step needs.
#Getting work: getTransaction
getTransaction returns the next Transaction, or null to tell the loop the queue is drained. It can poll, read a
workbook, read a table or slice an in-memory list. process-orders polls the demo app's pending endpoint and returns
the first row.
async getTransaction(ctx) {
const resp = await ctx.page.request.get('/api/orders/pending');
if (!resp.ok()) throw new SystemException(`Queue request failed with HTTP ${resp.status()}`, { code: 'QUEUE_HTTP_ERROR' });
const queue = await resp.json();
if (queue.length === 0) return null;
const next = queue[0];
return { id: next.id, payload: { ...next } };
}
The one rule is that it must not return a transaction that already reached a terminal outcome in this run. The loop guards against that anyway, but a queue that keeps handing back the same item is a defect in the automation or the queue.
#Processing work: processTransaction
processTransaction receives the context and the transaction and does the screen or data work. It reports failure by
throwing: a BusinessException for a rule the data broke, a SystemException for infrastructure that misbehaved. It
does not catch those itself.
#The context in a transaction
The RunContext is the same object every call receives. Inside a transaction the ones that matter most are ctx.page
and the capability facades for the work, ctx.log for structured notes, ctx.steps for naming the spans of work,
ctx.screenshot when a run needs a picture at a specific point and ctx.checkAborted for a long loop. Naming steps
is what turns a support line from "invoice INV-1042 failed" into "invoice INV-1042 failed while saving, step 5 of 5".
#How data flows through one transaction
process-invoices shows the path end to end. init reads the workbook once and keeps the queue in a session map keyed
by ctx.runId, so two runs in one process never share state. getTransaction returns the next unprocessed row and
increments the cursor. processTransaction reads typed fields off payload, applies the business rules and writes the
result through ctx.excel.append.
async processTransaction(ctx, tx) {
const invoice = tx.payload as ExcelRow;
const code = String(invoice.InvoiceNumber ?? '');
const amount = Number(invoice.Amount);
if (!/^INV-\d{4}$/.test(code)) {
throw new BusinessException(`Invoice number "${code}" is not in INV-XXXX format`, 'INVALID_INVOICE_NUMBER');
}
await ctx.excel.append(OUTPUT_FILE, [{ InvoiceNumber: code, Amount: amount, Status: 'Processed' }], { sheet: OUTPUT_SHEET });
}
The payload flows in, the rule runs, the outcome is written. When the rule throws, onBusinessException writes the
rejection to the same ledger with its reason. The loop then moves to the next invoice.
#Where the outcome is recorded
Each transaction produces a TransactionResult: txId, outcome (success, business-exception or
system-exception), attempts, the error when there was one, the recorded steps and failedStep. Those
results are collected into summary.json, which is the run's record of what happened unit by unit.
#Things that catch people out
- A session map must be keyed by
ctx.runId, not held on the module alone, or repeated runs bleed into each other. payloadmust be JSON-serializable. It is copied into the logs and into the summary.- Steps are flat. A step opened inside another step is refused with
STEP_NESTED. A step with no name is refused withSTEP_NAME_REQUIRED.