TaskSultan Docs tasksultan.com

Building automations

The anatomy of an automation

A walk through a real automation file, part by part, from its imports to its exception hooks.

An automation is a TypeScript file in automations/ that default-exports a defineAutomation call. The framework reads that object and runs it. This page opens process-orders, the example the repository ships. It reads the file from the top.

#What the file imports

Three imports do the work.

import { defineAutomation, BusinessException, SystemException } from '../framework';
import type { RunContext, Transaction } from '../framework';
import { useTarget } from '../targets';
import { config } from '../config/tasksultan.config';

defineAutomation is an identity helper: it types the object you hand it and returns the same object. The two exception classes are how a step says a unit of work failed. useTarget resolves a named Target from the object repository. config supplies the base URL.

Notice what is missing. There is no Playwright import, no fs and no selector. Driving the screen and reading secrets is the Robot's job, so the automation reaches those through the context the framework passes in.

#The definition object

The default export is a plain object. process-orders opens like this.

export default defineAutomation({
  id: 'process-orders',
  displayName: 'Process Orders',
  description: 'Logs in, pulls pending orders from the demo queue, updates and submits each order.',
  version: '0.1.0',
  capabilities: ['web'],
  url: config.demo.defaultUrl,
  retry: { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 2500, backoffFactor: 2 },
});

id is the stable name. It becomes the folder under artifacts/, the manifest id in a package and the identifier the CLI shows. The file stem must match it. displayName and description are for people. version is the author's own handle, bumped when the source changes.

capabilities is the declared list of engines the automation needs. ['web'] says the Runner must provide a browser, which it always does. A capability such as files or desktop behaves differently: the facade exists, but every call refuses by name until the automation declares it and the Robot supplies a provider.

url is the target application's base. retry is the policy for system failures. There is also an optional dialogs policy for native alert, confirm and prompt windows. When dialogs is absent, a dialog refuses the transaction by name rather than letting Playwright dismiss it with no decision recorded.

#The lifecycle functions

The rest of the object is the REFramework lifecycle, written as ordinary methods. init runs once before any work and is where this example resets demo data and signs in. getTransaction returns the next unit of work or null to end the run. processTransaction does the work for one transaction. onBusinessException and onSystemException run after a failure. onEnd runs once when the loop is over.

Inside processTransaction, process-orders calls four small named functions.

async processTransaction(ctx, tx) {
  ctx.log.info(`Processing order ${tx.id}`, { tx: tx.id });
  await openOrder(ctx, tx);
  await updateOrder(ctx, tx);
  await submitOrder(ctx, tx);
  await verifySubmitted(ctx, tx);
}

Those functions live below the definition, at module scope. Keeping each step as a named top-level call is deliberate: the Studio derives the workflow diagram from their order, so the diagram and the code cannot disagree. submitOrder shows both failure lanes in one place. A 500 from the order service throws a SystemException, which the framework retries. A 400 throws a BusinessException, which is never retried.

#Things that catch people out

  • The file must default-export the definition. A named export is ignored and the Robot refuses the run by name.
  • The id and the file stem have to match, or the registry will not load the file.
  • process-orders contains no selectors at all. Its rows, buttons and banners are addressed by Target name. Every target is registered in targets/index.ts, so adding one means registering it there.
  • init is also wrapped in the retry policy. A flaky login is retried, but a wrong credential is not: that refusal is non-retryable.