Polling Reaktors
A Polling Reaktor runs on a schedule in the background — processing queues, syncing data, or performing periodic maintenance. Unlike regular Reaktors that execute in response to a user action, Polling Reaktors execute on a timer and are controlled through REST endpoints on each App Instance.
Registering a Polling Reaktor
import { registerPollingReaktor, createAktorComposition, aktorIfElse, aktorConst } from '@operaide/aktor';
const aktorQueueProcessor = createAktorComposition('aktorQueueProcessor', () => {
return aktorIfElse({
if_: aktorHasPendingItems({}),
then_: aktorProcessNextItem({}), // Returns true if more items remain
else_: aktorConst(false), // No work — wait for next interval
});
});
registerPollingReaktor({
reaktorDefinitionId: 'queue-processor',
interval: '2m',
autoStart: false,
label: 'Queue Processor',
description: 'Processes pending items from the work queue',
aktor: () => aktorQueueProcessor({}),
});
The aktor prop is a factory that returns a fresh Aktor graph for each tick. Unlike registerReaktorDefinition — where aktor is the composition itself and the framework wires its inputs — a Polling Reaktor has no caller to supply inputs, so you invoke the composition yourself and return the resulting Aktor.
Configuration
| Option | Type | Default | Description |
|---|---|---|---|
reaktorDefinitionId | string | required | Unique identifier for this Polling Reaktor |
interval | string | required | Time between executions (see syntax below) |
autoStart | boolean | false | Initial run state when an App Instance first creates the job |
aktor | () => Aktor<boolean> | required | Factory that returns the Aktor implementing the polling logic |
label | string | — | Display name in the UI |
description | string | — | Description of what the Reaktor does |
Interval Syntax
The interval value is a string with a number and a unit:
| Example | Meaning |
|---|---|
'30ms' | 30 milliseconds |
'5s' | 5 seconds |
'2m' | 2 minutes |
'1h' | 1 hour |
'1d' | 1 day |
The Boolean Return Contract
The Aktor returned by the aktor factory must resolve to a boolean. The return value controls what happens after each execution:
| Return | Effect |
|---|---|
true | More work exists — execute again immediately |
false | No work — wait for the configured interval before running again |
This lets a Polling Reaktor drain a queue as fast as possible when there is work, then back off to the configured interval when idle.
A short minimum cycle time of about 250ms floors immediate refire. A Reaktor that returns true runs again as soon as it returns, but consecutive runs stay at least that far apart. Real per-run work is unaffected; the floor only bounds a Reaktor that returns true without doing work.
const aktorProcessQueue = createAktorComposition('aktorProcessQueue', () => {
const result = aktorProcessBatch({});
// result: Aktor<boolean>
// true → more items in queue, run again now
// false → queue empty, wait 2 minutes
return result;
});
One Job per App Instance
Each App Instance that installs the app gets its own polling job, identified by the App Instance and the reaktor name. Two App Instances of the same app poll independently, each with its own start/stop state and interval.
autoStartsets the initial state when the job is first created. Later changes to the default do not affect jobs that already exist.intervalis the starting value. An operator can override it per App Instance through the control API.- The platform runs the reaktor under the identity of whoever started it, or the App Instance owner for an
autoStartjob, so settings and permissions resolve as they would for a normal call. - A reaktor only polls when it is installed as part of an App Instance. A standalone deployed reaktor does not poll.
REST API
Control a Polling Reaktor through the App Instance REST API. The endpoints are scoped to the organization and App Instance, so each App Instance controls its own polling.
| Method | Endpoint | Description |
|---|---|---|
POST | /api/v2/orgs/{orgId}/apps/{instanceName}/polling/{reaktorName}/start | Start polling |
POST | /api/v2/orgs/{orgId}/apps/{instanceName}/polling/{reaktorName}/stop | Stop polling |
GET | /api/v2/orgs/{orgId}/apps/{instanceName}/polling/{reaktorName}/status | Get current status |
reaktorName is the short name from reaktorDefinitionId. Authenticate with your API key in the X-API-KEY header, the same as other App Instance calls. These endpoints let you control polling from external systems, CI/CD pipelines, or monitoring dashboards.
Full Example: Batch Email Processor
import {
registerPollingReaktor,
aktorConst,
createAktorComposition,
createAktorFunction,
aktorIfElse,
} from '@operaide/aktor';
import { aktorAppDatabase } from '@operaide/database';
const aktorHasPendingEmails = createAktorFunction('aktorHasPendingEmails',
async ({ client }: { client: any }) => {
const result = await client.execute('SELECT COUNT(*) as count FROM emails WHERE status = ?', ['pending']);
return result.rows[0].count > 0;
}
);
const aktorProcessNextEmail = createAktorFunction('aktorProcessNextEmail',
async ({ client }: { client: any }) => {
// Process one email and return whether more exist
await client.execute("UPDATE emails SET status = 'processing' WHERE id = (SELECT id FROM emails WHERE status = 'pending' LIMIT 1)");
// ... send the email ...
const remaining = await client.execute("SELECT COUNT(*) as count FROM emails WHERE status = 'pending'");
return remaining.rows[0].count > 0; // true = more work
}
);
const aktorEmailSender = createAktorComposition('aktorEmailSender', () => {
const db = aktorAppDatabase({});
return aktorIfElse({
if_: aktorHasPendingEmails({ client: db }),
then_: aktorProcessNextEmail({ client: db }),
else_: aktorConst(false),
});
});
registerPollingReaktor({
reaktorDefinitionId: 'email-sender',
interval: '30s',
autoStart: true,
label: 'Email Sender',
description: 'Sends pending emails from the queue',
aktor: () => aktorEmailSender({}),
});