TaskSultan Docs tasksultan.com

Building automations

Transactions and the transaction context

What one unit of work is, what the context carries into it and how a payload flows from the queue to the recorded result.

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.
  • payload must 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 with STEP_NAME_REQUIRED.