Scheduling & Programmatic Execution
Start and manage workflows programmatically from your NestJS application — in response to API requests, webhook events, cron jobs, or internal application logic — without going through the Studio UI.
Overview
Use the WorkflowRunner to execute workflows in response to:
- External API requests
- Webhook events
- Scheduled cron jobs
- Internal application events
- Batch processing tasks
For a runnable version of every pattern below — cron, webhook, delayed run, and batch — see the
@loopstack/scheduling-examples package. Each fundamental ships as a small
workflow plus the real trigger that fires it, and you can drive the HTTP-triggered ones straight from
Studio.
Basic Example
Create a Controller
import { Body, Controller, Post } from '@nestjs/common';
import { WorkflowRunner } from '@loopstack/core';
import { MyWorkflow } from './workflows/my.workflow';
@Controller()
export class AppController {
constructor(private readonly workflowRunner: WorkflowRunner) {}
@Post('run-my-workflow')
async runMyWorkflow(@Body() payload: any) {
const userId = '...'; // define a user id to run the workflow
const result = await this.workflowRunner.run(MyWorkflow, payload, {
appName: 'default',
userId,
});
return { message: 'Workflow run is queued.', workflowId: result.workflowId };
}
}WorkflowRunner Methods
// Async (queued via BullMQ)
await this.workflowRunner.run(
workflow, // Workflow class reference
args, // Data passed as workflow args (type-safe)
{
appName, // App name for workspace resolution
userId, // User ID for execution context
},
);
// Sync (inline execution, awaits result)
await this.workflowRunner.runSync(
workflow, // Workflow class reference
args, // Data passed as workflow args (type-safe)
{
appName, // App name for workspace resolution
userId, // User ID for execution context
stateless, // Optional: skip persistence (default: false)
},
);Advanced Examples
Webhook Handler
Trigger a workflow when receiving a webhook:
import { Body, Controller, Headers, Post } from '@nestjs/common';
import { WorkflowRunner } from '@loopstack/core';
@Controller('webhooks')
export class WebhookController {
constructor(private readonly workflowRunner: WorkflowRunner) {}
@Post('stripe')
async handleStripeWebhook(@Body() webhookData: any, @Headers('stripe-signature') signature: string) {
await this.workflowRunner.run(
ProcessWebhookWorkflow,
{
source: 'stripe',
event: webhookData,
receivedAt: new Date().toISOString(),
},
{
appName: 'main',
userId: webhookData.userId,
},
);
return { received: true };
}
}Scheduled Task
Execute a workflow on a schedule using NestJS’s @Cron decorator:
import { Injectable } from '@nestjs/common';
import { Cron, CronExpression } from '@nestjs/schedule';
import { WorkflowRunner } from '@loopstack/core';
@Injectable()
export class TaskScheduler {
constructor(private readonly workflowRunner: WorkflowRunner) {}
@Cron(CronExpression.EVERY_DAY_AT_9AM)
async generateDailyReports() {
const users = await this.getUsersWithReportsEnabled();
for (const user of users) {
await this.workflowRunner.run(
DailyReportWorkflow,
{
reportDate: new Date().toISOString(),
reportType: 'daily',
},
{
appName: 'reports',
userId: user.id,
},
);
}
}
}Delayed Run
Run a workflow once after a delay — e.g. “follow up 24h after signup”. Register a one-off timeout with NestJS’s SchedulerRegistry, then trigger the workflow when it fires:
import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { randomUUID } from 'node:crypto';
import { WorkflowRunner } from '@loopstack/core';
@Injectable()
export class SignupFollowupService {
constructor(
private readonly workflowRunner: WorkflowRunner,
private readonly scheduler: SchedulerRegistry,
) {}
scheduleFollowup(email: string, userId: string, delayMs = 24 * 60 * 60 * 1000) {
const name = `signup-followup-${randomUUID()}`;
const timeout = setTimeout(() => {
this.scheduler.deleteTimeout(name);
void this.workflowRunner.run(SignupFollowupWorkflow, { email }, { appName: 'onboarding', userId });
}, delayMs);
this.scheduler.addTimeout(name, timeout);
}
}SchedulerRegistry timeouts live in memory, so a server restart loses any pending run. For delays that must survive restarts, enqueue a BullMQ delayed job instead.
Batch Processing
Process multiple items by triggering workflows in parallel:
import { Injectable } from '@nestjs/common';
import { WorkflowRunner } from '@loopstack/core';
@Injectable()
export class OrderProcessingService {
constructor(private readonly workflowRunner: WorkflowRunner) {}
async processPendingOrders() {
const pendingOrders = await this.getPendingOrders();
const promises = pendingOrders.map((order) =>
this.workflowRunner.run(
ProcessOrderWorkflow,
{
orderId: order.id,
orderData: order,
},
{
appName: 'orders',
userId: order.userId,
},
),
);
await Promise.all(promises);
return { processed: pendingOrders.length };
}
}Registry References
- scheduling-examples — Runnable examples of every scheduling fundamental: cron (
@Cron+WorkflowRunner.run), webhook (@Public @Postcontroller), delayed run (SchedulerRegistrytimeout), and batch (Promise.allfan-out). Includes Studio-launchable workflows that make the real HTTP call to each trigger endpoint.