@loopstack/scheduling-examples
Scheduling workflow examples for the Loopstack automation framework.
This module demonstrates the scheduling fundamentals — the primitives you use to start workflows programmatically instead of clicking “Run” in Studio. Each fundamental ships as a small workflow (the work) plus the real trigger that fires it (the primitive): a cron schedule, a webhook endpoint, a delayed timeout, and a batch fan-out.
Background reading: Programmatic Execution.
The core primitive: WorkflowRunner
Every trigger boils down to injecting WorkflowRunner (from @loopstack/core, globally available
once LoopstackModule.forRoot() is imported) and calling:
run(WorkflowClass, args, { appName, userId })— enqueue on BullMQ, return aworkflowId. Used by all triggers here.runSync(WorkflowClass, args, { appName, userId, stateless? })— execute inline and await the result.
Background triggers (cron, webhooks, timeouts) have no HTTP request, so no user context. The
RunUserResolver helper picks the local Studio user to run as —
open Studio once so that user exists.
The fundamentals
| Fundamental | The primitive (where it lives) | The work |
|---|---|---|
| Cron | CronTriggerScheduler — @Cron(EVERY_MINUTE) | CronTriggerWorkflow posts a message |
| Webhook | WebhookTriggerController — @Public @Post('payment') | WebhookTriggerWorkflow records a receipt |
| Delayed | DelayedRunController — SchedulerRegistry one-off timeout | DelayedRunWorkflow sends a follow-up |
| Batch | BatchTriggerController — Promise.all fan-out | BatchTriggerWorkflow emails one recipient |
The trigger is where the scheduling primitive actually lives; the work workflow is just what it launches.
The webhook/delayed/batch controllers are marked
@Public()so you cancurlthem without a token. Real integrations should secure them — verify a provider signature on webhooks, require auth on batch/admin endpoints.
Run it all from Studio — no curl needed
Studio exposes three launchable workflows, one per HTTP-triggered fundamental. Each makes the real
HTTP POST to its endpoint — the same call an external caller would make — and posts the response:
| Run this in Studio | What it does |
|---|---|
CallWebhookWorkflow | POST /webhooks/scheduling-examples/payment → fires WebhookTriggerWorkflow |
CallSignupWorkflow | POST /webhooks/scheduling-examples/signup → schedules DelayedRunWorkflow |
CallNewsletterWorkflow | POST /webhooks/scheduling-examples/newsletter → fans out BatchTriggerWorkflow |
They go over real HTTP (fetch to http://localhost:$PORT), so they prove the whole trigger path,
not just the workflow body. The cron fundamental needs no launcher — it fires on its own schedule. The
curl commands below do the same thing from a terminal. The launched “work” workflows
(WebhookTriggerWorkflow, etc.) are registered but not listed as standalone Studio entries.
Cron — @Cron
CronTriggerScheduler.tick() is a NestJS @Cron(CronExpression.EVERY_MINUTE) method that calls
WorkflowRunner.run(CronTriggerWorkflow, …). It is off by default so it doesn’t spam your
workspace — enable it with:
SCHEDULING_EXAMPLES_CRON_ENABLED=true@Cron(CronExpression.EVERY_MINUTE)
async tick() {
if (this.configService.get('SCHEDULING_EXAMPLES_CRON_ENABLED') !== 'true') return;
const userId = await this.runUser.resolve();
if (!userId) return;
await this.workflowRunner.run(CronTriggerWorkflow, { message: 'hello world' }, { appName, userId });
}Swap CronExpression.EVERY_MINUTE for a cron string like '0 9 * * MON' for “every Monday at 9am”.
Webhook — @Public @Post
curl -X POST http://localhost:3000/webhooks/scheduling-examples/payment \
-H 'content-type: application/json' \
-d '{"customerEmail":"grace@example.com","amountCents":4200,"currency":"USD"}'The controller maps the request body to workflow args and calls run(), returning the workflowId.
Delayed — SchedulerRegistry timeout
curl -X POST http://localhost:3000/webhooks/scheduling-examples/signup \
-H 'content-type: application/json' -d '{"email":"grace@example.com"}'Registers a one-off timeout (default 10s, override with SCHEDULING_EXAMPLES_FOLLOWUP_DELAY_MS) that
fires the follow-up workflow later — the “run this in 24h after signup” pattern, sped up for the demo.
SchedulerRegistrytimeouts live in memory: a server restart loses any pending follow-up. For a durable long delay, enqueue a BullMQ delayed job instead.
Batch — Promise.all fan-out
curl -X POST http://localhost:3000/webhooks/scheduling-examples/newsletter \
-H 'content-type: application/json' \
-d '{"recipients":["ada@example.com","grace@example.com"]}'Launches one workflow run per recipient in parallel and returns all the workflowIds.
Using in your app
import { SchedulingExamplesModule } from '@loopstack/scheduling-examples';
@Module({
imports: [LoopstackModule.forRoot(), SchedulingExamplesModule],
})
export class AppModule {}About
Author: Jakob Klippel
License: MIT