Build a recurring task scheduler using a Firestore collection of scheduled_tasks with cron expressions, a Cloud Function dispatcher that runs every minute to check for due tasks, and an admin UI for creating and managing scheduled jobs. Each task execution logs to an execution_log subcollection with success or failure status. Use cases include daily report generation, weekly email digests, hourly data syncs, and monthly cleanup routines.
Building a Custom Task Scheduler in FlutterFlow
Many apps need recurring background tasks: sending daily digests, syncing external data hourly, generating monthly reports, or cleaning up stale records. This tutorial builds a flexible task scheduler where tasks are defined in Firestore with cron-like schedules, a dispatcher Cloud Function checks for due tasks every minute, and an admin interface lets you create, edit, pause, and monitor scheduled jobs.
Prerequisites
- A FlutterFlow project on the Pro plan or higher
- Firebase project on the Blaze plan with Cloud Functions enabled
- Basic understanding of cron expressions (e.g., '0 8 * * *' means daily at 8 AM)
- Cloud Functions for each task action already deployed (e.g., sendDailyReport, syncExternalData)
Step-by-step guide
Design the Firestore schema for scheduled tasks and execution logs
Design the Firestore schema for scheduled tasks and execution logs
Create a `scheduled_tasks` collection with fields: name (String), description (String), schedule (String — cron expression like '0 8 * * *'), actionName (String — the Cloud Function to call), parameters (Map — JSON parameters to pass to the function), isActive (bool), lastRunAt (Timestamp), nextRunAt (Timestamp), createdBy (String), and createdAt (Timestamp). Add a subcollection `scheduled_tasks/{taskId}/execution_log` with fields: executedAt (Timestamp), status (String — success or failed), durationMs (int), error (String — null on success), and result (String — summary of what happened).
Expected result: The schema supports flexible task scheduling with full execution history per task.
Build the dispatcher Cloud Function that runs every minute
Build the dispatcher Cloud Function that runs every minute
Create a scheduled Cloud Function named taskDispatcher that runs every minute via Cloud Scheduler. The function queries scheduled_tasks where isActive is true and nextRunAt is less than or equal to the current time. For each due task, call the corresponding Cloud Function by name (using a switch statement or dynamic function lookup), pass the task's parameters, record the start time, and wrap the call in a try-catch. On success, log status 'success' to the execution_log subcollection and update lastRunAt. On failure, log the error message. After execution, calculate the next run time from the cron expression and update nextRunAt on the task document.
1// Cloud Function: taskDispatcher2const functions = require('firebase-functions');3const admin = require('firebase-admin');4admin.initializeApp();5const db = admin.firestore();67// Import your task action functions8const actions = require('./taskActions');910exports.taskDispatcher = functions.pubsub11 .schedule('every 1 minutes')12 .onRun(async () => {13 const now = admin.firestore.Timestamp.now();14 const dueTasks = await db15 .collection('scheduled_tasks')16 .where('isActive', '==', true)17 .where('nextRunAt', '<=', now)18 .get();1920 for (const doc of dueTasks.docs) {21 const task = doc.data();22 const start = Date.now();2324 try {25 const actionFn = actions[task.actionName];26 if (!actionFn) throw new Error(27 `Unknown action: ${task.actionName}`);2829 const result = await actionFn(task.parameters);3031 await doc.ref.collection('execution_log').add({32 executedAt: admin.firestore.FieldValue33 .serverTimestamp(),34 status: 'success',35 durationMs: Date.now() - start,36 result: result || 'Completed',37 });38 } catch (err) {39 await doc.ref.collection('execution_log').add({40 executedAt: admin.firestore.FieldValue41 .serverTimestamp(),42 status: 'failed',43 durationMs: Date.now() - start,44 error: err.message,45 });46 }4748 // Update lastRunAt and calculate nextRunAt49 const nextRun = getNextCronTime(task.schedule);50 await doc.ref.update({51 lastRunAt: admin.firestore.FieldValue52 .serverTimestamp(),53 nextRunAt: admin.firestore.Timestamp54 .fromDate(nextRun),55 });56 }57 });Expected result: Every minute, the dispatcher finds due tasks, executes them, logs the result, and schedules the next run.
Implement cron expression parsing for next run calculation
Implement cron expression parsing for next run calculation
Add the cron-parser npm package to your Cloud Functions. Create a helper function getNextCronTime that takes a cron expression string and returns the next Date when the task should run. Use the cron-parser library's parseExpression method with the currentDate set to now. Call interval.next().toDate() to get the next occurrence. Handle invalid cron expressions with a try-catch that defaults to 24 hours from now and logs a warning. This function is called by the dispatcher after each execution to set the nextRunAt field.
1// Helper: getNextCronTime2const cronParser = require('cron-parser');34function getNextCronTime(cronExpression) {5 try {6 const interval = cronParser.parseExpression(7 cronExpression,8 { currentDate: new Date() }9 );10 return interval.next().toDate();11 } catch (err) {12 console.warn(13 `Invalid cron: ${cronExpression}`, err14 );15 // Default: 24 hours from now16 const fallback = new Date();17 fallback.setHours(fallback.getHours() + 24);18 return fallback;19 }20}Expected result: The helper correctly calculates the next run time from any valid cron expression and handles invalid expressions gracefully.
Build the admin UI for creating and managing scheduled tasks
Build the admin UI for creating and managing scheduled tasks
Create a TaskScheduler admin page gated by role check. At the top, add a 'Create Task' Button that opens a BottomSheet with a form: name TextField, description TextField, schedule input (use a Row of DropDowns for frequency: every X minutes/hours/days or specific time, which generates the cron expression), actionName DropDown (populated from a config document listing available action functions), and a parameters TextField (JSON format). On submit, create the scheduled_tasks document with isActive true and nextRunAt calculated from the cron expression using a Custom Function. Below the form, display a ListView of all tasks from a Backend Query ordered by name. Each task shows name, schedule description, last run status, next run time, and an isActive Switch toggle.
Expected result: Admins can create new scheduled tasks using a user-friendly form, view all tasks, and toggle them active or inactive.
Add execution log viewing and task monitoring
Add execution log viewing and task monitoring
When an admin taps a task in the list, navigate to a TaskDetail page passing the taskId. Display the task configuration at the top, then a ListView of the execution_log subcollection ordered by executedAt descending. Each log entry shows the execution timestamp, status badge (green for success, red for failed), duration in milliseconds, and the result or error message. Add summary statistics at the top: total executions, success rate percentage, average duration, and last failure timestamp. Add a 'Run Now' Button that updates the task's nextRunAt to now, causing the dispatcher to pick it up on its next run. Add a 'Delete Task' Button with a confirmation dialog.
Expected result: Admins see detailed execution history per task with success rates, can trigger immediate execution, and can delete tasks.
Complete working example
1// Cloud Functions: Task Scheduler System2const functions = require('firebase-functions');3const admin = require('firebase-admin');4const cronParser = require('cron-parser');5admin.initializeApp();67const db = admin.firestore();89// Task action registry — add your functions here10const taskActions = {11 sendDailyReport: async (params) => {12 // Your daily report logic13 return 'Report sent to ' + (params.email || 'admin');14 },15 syncExternalData: async (params) => {16 // Your sync logic17 return 'Synced ' + (params.source || 'default');18 },19 cleanupStaleData: async (params) => {20 const cutoff = new Date();21 cutoff.setDate(cutoff.getDate() - (params.days || 30));22 // Delete old documents logic23 return 'Cleaned up records older than ' + params.days + ' days';24 },25};2627function getNextCronTime(expression) {28 try {29 const interval = cronParser.parseExpression(30 expression, { currentDate: new Date() }31 );32 return interval.next().toDate();33 } catch (err) {34 const fallback = new Date();35 fallback.setHours(fallback.getHours() + 24);36 return fallback;37 }38}3940// Dispatcher: runs every minute41exports.taskDispatcher = functions.pubsub42 .schedule('every 1 minutes')43 .onRun(async () => {44 const now = admin.firestore.Timestamp.now();45 const due = await db46 .collection('scheduled_tasks')47 .where('isActive', '==', true)48 .where('nextRunAt', '<=', now)49 .get();5051 const promises = due.docs.map(async (doc) => {52 const task = doc.data();53 const start = Date.now();54 let status = 'success';55 let result = '';56 let error = null;5758 try {59 const fn = taskActions[task.actionName];60 if (!fn) throw new Error('Unknown action');61 result = await fn(task.parameters || {});62 } catch (e) {63 status = 'failed';64 error = e.message;65 }6667 const logEntry = {68 executedAt: admin.firestore.FieldValue69 .serverTimestamp(),70 status,71 durationMs: Date.now() - start,72 ...(result && { result }),73 ...(error && { error }),74 };7576 const nextRun = getNextCronTime(task.schedule);7778 await Promise.all([79 doc.ref.collection('execution_log')80 .add(logEntry),81 doc.ref.update({82 lastRunAt: admin.firestore.FieldValue83 .serverTimestamp(),84 nextRunAt: admin.firestore.Timestamp85 .fromDate(nextRun),86 }),87 ]);88 });8990 await Promise.all(promises);91 });Common mistakes when building a Custom Task Scheduler in FlutterFlow
Why it's a problem: Running the dispatcher every minute with many simultaneous tasks causing cold start delays
How to avoid: For critical tasks that must run exactly on time, create dedicated Cloud Scheduler jobs (one per task) instead of routing through a single dispatcher. Reserve the dispatcher for non-time-critical tasks.
Why it's a problem: Storing the cron expression without calculating and storing nextRunAt
How to avoid: Calculate nextRunAt whenever a task is created, edited, or executed. The dispatcher simply queries for tasks where nextRunAt <= now, which is a fast indexed Firestore query.
Why it's a problem: Not handling task execution failures with retry logic
How to avoid: Add a retryCount field to each task. On failure, if retryCount is below a max (e.g., 3), set nextRunAt to 5 minutes from now instead of the next cron interval. Reset retryCount to 0 on success.
Best practices
- Pre-calculate and store nextRunAt as a Firestore Timestamp for efficient querying instead of parsing cron expressions on every dispatcher run
- Log every execution with status, duration, and result or error message for complete observability
- Use the cron-parser npm package instead of writing custom cron parsing logic — it handles edge cases like DST transitions
- Process due tasks in parallel with Promise.all to minimize the dispatcher function execution time
- Add a 'Run Now' feature that sets nextRunAt to the current time for on-demand task execution
- Keep task action functions idempotent so that accidental double-execution (from dispatcher retries) does not cause problems
- Monitor the dispatcher function itself for failures — if the dispatcher goes down, no tasks run
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a custom task scheduler in FlutterFlow. I need a Firestore schema for scheduled tasks with cron expressions, a Cloud Function dispatcher that runs every minute and executes due tasks, cron parsing to calculate next run times, and execution logging. Show me the complete Cloud Functions code and Firestore schema.
Create an admin page for managing scheduled tasks. Show a list of tasks with name, cron schedule, last run status, and next run time. Add a form to create new tasks with name, description, frequency selector, and action name. Each task should have a switch to enable or disable it.
Frequently asked questions
What is a cron expression and how do I write one?
A cron expression defines a schedule using five fields: minute, hour, day-of-month, month, day-of-week. For example, '0 8 * * *' means every day at 8:00 AM, '*/30 * * * *' means every 30 minutes, and '0 0 * * 1' means every Monday at midnight.
Can the dispatcher handle hundreds of tasks?
The dispatcher Cloud Function has a 9-minute timeout. For hundreds of tasks, process them in parallel with Promise.all. If you have thousands of tasks, split them across multiple dispatcher instances or use Google Cloud Tasks for fan-out.
How do I add a new task action without redeploying Cloud Functions?
You cannot avoid redeployment for new Cloud Function code. However, you can make actions configurable by creating generic actions like 'httpCall' that send HTTP requests to configurable endpoints. New tasks only need a new URL, not new code.
What happens if the dispatcher itself fails?
Cloud Scheduler retries the dispatcher on failure. Additionally, tasks that were due but not executed will still have nextRunAt in the past, so the next successful dispatcher run will pick them up. No tasks are permanently lost.
Can I schedule tasks to run at specific times in different time zones?
Yes. Store a timeZone field on each task (e.g., 'America/New_York'). Pass the timezone option to cron-parser when calculating the next run time. This ensures tasks run at the correct local time regardless of DST changes.
Can RapidDev help build an enterprise-grade task scheduling system?
Yes. RapidDev can implement distributed task scheduling with priority queues, dependency chains, retry policies, dead letter queues, and real-time monitoring dashboards.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation