Schedule recurring tasks in Retool using Retool Workflows with a Schedule trigger. Create a Workflow, click the Trigger block, select 'Schedule', and configure a cron expression (e.g. '0 8 * * *' for 8 AM daily). Add query blocks, code blocks, and notification blocks to define what the workflow does. Workflows run server-side without a user present.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 20-25 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Automate Recurring Tasks with Retool Workflows
Retool Workflows is Retool's built-in automation engine. It lets you run scheduled jobs, process background tasks, and respond to webhooks without needing a separate cron service or Lambda function. Workflows run entirely server-side — they can query databases, call APIs, send emails, and execute JavaScript even when no users are in the Retool app.
Common scheduled tasks include: daily data sync between systems, weekly summary reports emailed to stakeholders, nightly cleanup of expired records, hourly monitoring queries that trigger alerts when thresholds are exceeded, and data transformation pipelines that run on a schedule.
This tutorial covers creating scheduled workflows, writing cron expressions, building multi-step automation logic, and triggering workflows from Retool apps on demand.
Prerequisites
- Retool account on a paid plan with Workflows access
- Understanding of what the automated task needs to do (query, transform, notify, etc.)
- Optional: familiarity with cron syntax for custom schedules
- For app-triggered workflows: an existing Retool app
Step-by-step guide
Create a New Retool Workflow
Navigate to the Workflows section from the Retool main sidebar. Click '+ New Workflow'. Give it a descriptive name like 'daily-report-workflow' or 'inventory-sync'. The Workflow editor opens with a blank canvas showing an initial Trigger block. Workflows are separate from Retool apps — they have their own editor with a block-based flow designer. Each block is a discrete operation: query, code, condition, email, HTTP request, etc.
Expected result: A new Workflow editor opens with an empty canvas.
Configure the Schedule Trigger
Click the Trigger block at the top of the canvas. The trigger configuration panel opens on the right. Select 'Schedule' as the trigger type. Choose between preset intervals (every N minutes, hourly, daily, weekly) or enter a custom cron expression for more control. The cron expression follows standard 5-field syntax: minute hour day-of-month month day-of-week. Timezone selection is important — Retool executes cron in UTC by default. Adjust your time appropriately.
1// Common cron expressions:2// Every hour at top of hour: 0 * * * *3// Daily at 8 AM UTC: 0 8 * * *4// Daily at 9 AM Eastern: 0 14 * * * (9 AM EST = 14:00 UTC)5// Weekdays at 8 AM UTC: 0 8 * * 1-56// Every Monday at 9 AM: 0 9 * * 17// First day of month: 0 8 1 * *8// Every 15 minutes: */15 * * * *9// Twice daily (8 AM + 8 PM): 0 8,20 * * *1011// Cron syntax: minute hour day month weekday12// 0 = Sunday, 1 = Monday, ... 6 = SaturdayExpected result: The trigger is configured to fire at the specified schedule. The workflow will activate when you publish it.
Add a Database Query Block
Click the '+' button below the Trigger block to add a new block. Select 'Resource query' (or 'Query') to add a database query. Choose your resource from the dropdown and write your SQL. In Workflows, query results are accessed in subsequent blocks as blocks.queryBlockName.data. Block names are set by clicking the block name label at the top of each block.
1-- Example: daily cleanup query (block name: deleteExpiredSessions)2DELETE FROM user_sessions3WHERE expires_at < NOW()4RETURNING id;56-- Example: daily report query (block name: getDailyStats)7SELECT8 DATE_TRUNC('day', created_at)::date AS date,9 COUNT(*) AS order_count,10 SUM(total_amount) AS revenue,11 COUNT(*) FILTER (WHERE status = 'refunded') AS refunds12FROM orders13WHERE created_at >= CURRENT_DATE - INTERVAL '1 day'14 AND created_at < CURRENT_DATE15GROUP BY 1;1617-- Example: sync check (block name: getUnsyncedRecords)18SELECT id, data, created_at19FROM queue_items20WHERE synced = false21 AND created_at < NOW() - INTERVAL '5 minutes'22ORDER BY created_at ASC23LIMIT 100;Expected result: The query block is configured and returns data when tested manually with the 'Run' button in the Workflow editor.
Add a Code Block for Custom Logic
Add a Code block (JavaScript) after your query for data transformation, conditional logic, or formatting. Code blocks in Workflows work similarly to JS Queries in apps but have access to blocks.blockName.data to reference previous block outputs. Code blocks support async/await and can access Retool configuration variables via retoolContext.configVars.NAME.
1// Code block: buildReport2// Processes getDailyStats.data into a formatted report34const stats = blocks.getDailyStats.data;56if (!stats || stats.length === 0) {7 return { hasData: false, message: 'No orders yesterday.' };8}910const day = stats[0];11const revenueFormatted = Number(day.revenue || 0).toLocaleString('en-US', {12 style: 'currency', currency: 'USD'13});1415const reportLines = [16 `Date: ${day.date}`,17 `Orders: ${day.order_count}`,18 `Revenue: ${revenueFormatted}`,19 `Refunds: ${day.refunds}`20];2122return {23 hasData: true,24 summary: reportLines.join('\n'),25 revenue: Number(day.revenue || 0),26 orderCount: Number(day.order_count || 0)27};Expected result: The code block processes query data and returns a structured result used by downstream blocks.
Add a Condition Block for Branching Logic
Condition blocks let you branch the workflow based on data conditions. This is essential for scheduled workflows — only send a report if there's data, only run cleanup if there are expired records, only sync if the queue is non-empty. Add a Condition block and configure its expression using block output references.
1// Condition block: hasDataToProcess2// Expression (True/False):3{{ blocks.buildReport.data.hasData === true }}45// Connect True branch → notification blocks6// Connect False branch → End (or a log block)78// Another example: alert only if revenue dropped significantly9{{ blocks.getDailyStats.data[0]?.revenue < 1000 }}10// True → send low-revenue alert11// False → send normal reportExpected result: The workflow branches correctly based on data conditions, skipping unnecessary blocks when there's nothing to process.
Set Up a Webhook Trigger for On-Demand Execution
In addition to scheduled triggers, Workflows support a Webhook trigger that lets you execute the workflow on demand via HTTP POST. This is useful for triggering workflows from external systems or from your Retool app. Change the trigger type to 'Webhook' to get a unique URL. Send a POST request to that URL to start the workflow. You can pass payload data in the POST body, accessible in blocks as trigger.data.
1// Webhook trigger URL: https://api.retool.com/v1/workflows/abc123/startTrigger/...2// POST body (from external system or Retool app):3{4 "userId": 456,5 "reportType": "monthly",6 "sendTo": "manager@company.com"7}89// Access in Workflow blocks:10// {{ trigger.data.userId }}11// {{ trigger.data.reportType }}1213// From a Retool app, trigger via Resource Query:14// Resource: Retool Workflows15// Select workflow and configure inputs1617// Or via an HTTP Request from a JS Query:18await fetch('https://api.retool.com/v1/workflows/abc123/startTrigger/...', {19 method: 'POST',20 headers: { 'Content-Type': 'application/json' },21 body: JSON.stringify({ userId: currentUserId.value })22});Expected result: The workflow can be triggered on demand via the webhook URL from any external system or Retool app.
Activate and Monitor the Workflow
Once your workflow is built and tested, activate it by clicking the 'Activate' or 'Publish' button in the Workflow editor. Inactive workflows do not run on schedule even if configured. Monitor workflow execution in the Workflow's 'Run History' tab. Each execution shows its status (success/failed), duration, and the output of each block. When a workflow fails, click the failed run to see which block failed and what the error was.
Expected result: The workflow is active, shows runs in the Run History, and executes on the configured schedule.
Add Error Handling in Workflows
Workflows can fail silently if a block throws an error and there's no error handling. Add error handling by: (1) using try/catch in Code blocks, (2) configuring block-level 'On failure' behavior (continue vs stop), or (3) adding a parallel error path that sends an alert when the workflow fails. Configure critical workflows to notify you on failure using the Workflow settings panel.
1// Code block with error handling:2try {3 const records = blocks.getUnsyncedRecords.data;45 const results = await Promise.allSettled(6 records.map(async (record) => {7 // Process each record8 const response = await fetch('https://api.external.com/sync', {9 method: 'POST',10 headers: { 'Content-Type': 'application/json',11 'Authorization': `Bearer ${retoolContext.configVars.API_KEY}` },12 body: JSON.stringify(record.data)13 });14 return { id: record.id, success: response.ok };15 })16 );1718 const successful = results.filter(r => r.status === 'fulfilled' && r.value.success);19 const failed = results.filter(r => r.status === 'rejected' || !r.value?.success);2021 return {22 synced: successful.length,23 failed: failed.length,24 errors: failed.map(f => f.reason?.message || 'Unknown error')25 };26} catch (err) {27 // Return error data instead of throwing (prevents workflow from halting)28 return { synced: 0, failed: -1, criticalError: err.message };29}Expected result: The workflow handles errors gracefully, continuing to process other items even when individual operations fail.
Complete working example
1// Workflow: dailyReportWorkflow2// Trigger: Schedule — 0 8 * * 1-5 (8 AM UTC, weekdays)3// Purpose: Send daily order summary to ops team45// Block 1: getDailyOrders (Resource Query - PostgreSQL)6SELECT7 COUNT(*) AS total_orders,8 SUM(total_amount) AS revenue,9 COUNT(*) FILTER (WHERE status = 'pending') AS pending,10 COUNT(*) FILTER (WHERE status = 'completed') AS completed,11 COUNT(*) FILTER (WHERE status = 'refunded') AS refunded12FROM orders13WHERE created_at >= NOW() - INTERVAL '24 hours';1415// Block 2: buildReportMessage (Code - JavaScript)16const data = blocks.getDailyOrders.data[0] || {};17const yesterday = new Date();18yesterday.setDate(yesterday.getDate() - 1);19const dateStr = yesterday.toLocaleDateString('en-US', {20 weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'21});2223const revenue = Number(data.revenue || 0).toLocaleString('en-US', {24 style: 'currency', currency: 'USD'25});2627return {28 subject: `Daily Report: ${data.total_orders || 0} orders on ${dateStr}`,29 html: `30 <h2>Daily Order Report — ${dateStr}</h2>31 <table border="1" cellpadding="8">32 <tr><td><b>Total Orders</b></td><td>${data.total_orders || 0}</td></tr>33 <tr><td><b>Revenue</b></td><td>${revenue}</td></tr>34 <tr><td><b>Pending</b></td><td>${data.pending || 0}</td></tr>35 <tr><td><b>Completed</b></td><td>${data.completed || 0}</td></tr>36 <tr><td><b>Refunded</b></td><td>${data.refunded || 0}</td></tr>37 </table>38 `,39 hasOrders: (data.total_orders || 0) > 040};4142// Block 3: shouldSendReport (Condition)43// {{ blocks.buildReportMessage.data.hasOrders }}4445// Block 4: sendReport (Email)46// To: ops-team@yourcompany.com47// Subject: {{ blocks.buildReportMessage.data.subject }}48// Body: {{ blocks.buildReportMessage.data.html }} (HTML mode)Common mistakes
Why it's a problem: Building a workflow but not activating it, then wondering why it never runs
How to avoid: Workflows must be explicitly activated (published) after building. An inactive workflow shows 'Inactive' status and will not run on schedule. Click 'Activate' in the Workflow editor.
Why it's a problem: Using UTC time in cron without accounting for the recipient's timezone
How to avoid: If users expect reports at 8 AM Eastern, set your cron to 8 AM EST/EDT which is 13:00 or 12:00 UTC depending on daylight saving time. Use a timezone converter and note that UTC doesn't observe daylight saving time.
Why it's a problem: Accessing component values like textInput1.value in a Workflow Code block
How to avoid: Workflow Code blocks are server-side and have no access to Retool app components. They can only access block output (blocks.blockName.data), trigger data (trigger.data), and configuration variables (retoolContext.configVars).
Why it's a problem: Not adding error handling, causing the entire workflow to fail silently when one item fails
How to avoid: Wrap risky operations in try/catch inside Code blocks and return error information rather than throwing. Use Promise.allSettled() instead of Promise.all() when processing multiple items in parallel to handle individual failures gracefully.
Best practices
- Always test workflows manually with the 'Run' button before activating the schedule — this saves time compared to waiting for the next scheduled run.
- Add a Condition block early in the workflow to exit early if there is nothing to process — avoid sending empty reports or pointless notifications.
- Store all API keys, webhook URLs, and secrets in Retool configuration variables accessed via retoolContext.configVars — never hardcode credentials in workflow blocks.
- Use descriptive block names (getDailyOrders, buildReport, sendEmail) instead of default names — block output references are cleaner and easier to maintain.
- For long-running workflows, break complex logic into multiple Code blocks rather than one giant block — this makes debugging easier since you can see each block's output.
- Configure 'On failure' email notifications in Workflow settings so your team is alerted when critical scheduled workflows fail.
- Use RETURNING or record counts in SQL mutation queries to confirm rows were actually affected — log this in the workflow for audit trails.
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to create a Retool Workflow that runs every weekday at 8 AM UTC. It should: (1) query yesterday's orders from PostgreSQL (count, revenue, pending/completed/refunded breakdown), (2) check if there were any orders at all using a Condition block, (3) format the data into an HTML email report, (4) send the report to ops-team@company.com with a subject line including the date and order count. Write the SQL query for block 1, the JavaScript code block for step 3, and show the cron expression for the schedule trigger.
Create a Retool Workflow named 'dailyOrderReport' with: (1) Schedule trigger for 8 AM UTC weekdays (cron: 0 8 * * 1-5), (2) PostgreSQL query block named 'getDailyOrders' selecting yesterday's order stats (total count, revenue, by status), (3) Condition block that exits if total_orders is 0, (4) Code block named 'buildMessage' that formats the data as an HTML table, (5) Email block sending to ops@company.com with subject 'Daily Report: N orders'. Include proper block naming and show how to reference block outputs as blocks.blockName.data.
Frequently asked questions
Do Retool Workflows run even when nobody is logged into Retool?
Yes. Retool Workflows run server-side and are completely independent of whether any users are logged in. Scheduled workflows fire based on the cron schedule regardless of user activity. This is what makes them suitable for nightly jobs, data syncs, and automated reports.
What is the minimum schedule interval for a Retool Workflow?
Retool Workflows support schedules as frequent as every 1 minute (cron: */1 * * * *). However, very frequent workflows (under 5 minutes) can consume significant compute resources and may be subject to rate limits depending on your plan. For true real-time processing, consider a webhook trigger rather than polling.
Can a Retool Workflow call another Workflow?
Not directly within the Workflow block builder. However, you can trigger another workflow via an HTTP Request block pointing to the second workflow's webhook URL. Store the target webhook URL in a configuration variable and POST to it from the HTTP Request block.
How do I debug a Retool Workflow that is failing silently?
Check the Run History tab on the Workflow page — every execution (scheduled or manual) is logged with status and duration. Click a failed run to see which block failed and the error message. Also click 'Run' in the Workflow editor to trigger a test run and watch each block execute step by step with visible outputs.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation