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.
Use in Your App
Copy this directory into your app, then install what it imports:
npm install @loopstack/common @loopstack/coreRegister the module:
import { Module } from '@nestjs/common';import { LoopstackModule } from '@loopstack/loopstack-module';import { SchedulingExamplesModule } from './scheduling/scheduling-examples.module';
@Module({ imports: [LoopstackModule.forRoot(), SchedulingExamplesModule],})export class AppModule {}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 './scheduling/scheduling-examples.module';
@Module({ imports: [LoopstackModule.forRoot(), SchedulingExamplesModule],})export class AppModule {}About
Author: Jakob Klippel
License: MIT