Scheduling AI Jobs in n8n
n8n scheduled jobs let you run a workflow automatically on a timer instead of waiting for a webhook or a manual click, and that timer becomes the backbone of most useful AI automation: a daily summary, a nightly data enrichment pass, an hourly alert check. The core building block is the Schedule Trigger node, which fires your workflow on a cron expression or interval and then hands off to whatever nodes you connect after it, including an HTTP Request node calling an LLM API or a dedicated AI node. The tricky part is not adding the trigger, it is making the rest of the workflow survive running unattended: handling API failures, controlling token spend, and knowing when a run silently did nothing useful.
This article walks through building, hardening, and monitoring n8n scheduled jobs that involve an AI call, with concrete node configurations and code you can copy into your own workflow.
How n8n scheduled jobs work: the Schedule Trigger node
Every scheduled workflow in n8n starts with the Schedule Trigger node (in older versions this was the Cron node, now deprecated in favor of Schedule Trigger, though existing Cron nodes still run). Drop it as the first node in a workflow and it exposes a few trigger modes:
- Seconds / Minutes / Hours / Days / Weeks / Months: pick an interval directly in the UI, no cron syntax needed.
- Cron Expression: full five-field cron syntax for anything the interval dropdowns cannot express.
- Custom (Cron): n8n's cron parser supports the standard fields,
minute hour day month weekday.
A daily 7 AM run looks like this in cron syntax:
0 7 * * *An every-15-minutes check, useful for near-real-time AI monitoring jobs, looks like this:
*/15 * * * *Two details trip people up constantly:
- Timezone: n8n resolves the Schedule Trigger against the timezone set in your instance settings (
Settings -> Workflow settingsor theGENERIC_TIMEZONEenvironment variable on self-hosted instances). If your server runs UTC and you expect 7 AM in your local time, your job fires at the wrong hour until you set this explicitly. - Trigger-only execution: the Schedule Trigger node has no input data. If you need context, like "yesterday's date" or "last run's timestamp," you compute it in the next node with an expression, not in the trigger itself.
A minimal date computation right after the trigger, using a Set node or a Code node:
const now = new Date();
const yesterday = new Date(now);
yesterday.setDate(now.getDate() - 1);
return [{
json: {
runDate: now.toISOString(),
windowStart: yesterday.toISOString(),
}
}];That gives every downstream node, including your AI call, a stable reference point instead of re-deriving "now" inconsistently across the workflow.
Building your first n8n scheduled job for an AI task
A common pattern for teams starting out: pull data on a schedule, summarize it with an LLM, and post the summary somewhere. Take a daily engineering digest as the example.
Workflow shape:
Schedule Trigger (daily, 8 AM)
-> HTTP Request (fetch yesterday's closed issues from your tracker's API)
-> Code node (format issue list into a prompt)
-> HTTP Request or AI node (call the LLM)
-> Code node (parse response)
-> Slack node (post digest to a channel)The fetch step is a plain HTTP Request node hitting your issue tracker's REST API with a date filter built from the Code node output above. The formatting step turns raw JSON into a compact prompt:
const issues = $input.first().json.items || [];
const bulletList = issues
.map(i => `- ${i.title} (closed by ${i.assignee || "unassigned"})`)
.join("\n");
const prompt = `Summarize the following closed issues from the last 24 hours into a 5-bullet digest for an engineering team. Group related items. Keep it under 150 words.\n\nIssues:\n${bulletList}`;
return [{ json: { prompt } }];For the AI call itself, n8n ships dedicated nodes for the major model providers (an Anthropic node for Claude, an OpenAI node, and generic HTTP Request as a fallback for anything without a native node). Using the HTTP Request node against the Claude API directly keeps the workflow portable and easy to reason about:
- Method: POST
- URL: the Claude Messages API endpoint
- Authentication: header auth with your API key stored in n8n credentials, never hardcoded in the node
- Body (JSON):
{
"model": "claude-opus-4-6",
"max_tokens": 400,
"messages": [
{ "role": "user", "content": "={{ $json.prompt }}" }
]
}The ={{ $json.prompt }} syntax is an n8n expression: the leading = tells n8n the field is dynamic, and $json.prompt pulls the value from the previous node's output. This is the single most important n8n concept for AI workflows, every prompt, every parsed response, every conditional branch runs through these expressions.
After the call, a Code node extracts the text and hands it to Slack:
const response = $input.first().json;
const text = response.content?.[0]?.text || "No summary generated.";
return [{ json: { summary: text } }];That is a complete, working scheduled AI job. The rest of this article is about what breaks when you leave it running for a month.
Debugging n8n scheduled jobs that call AI models
Unattended workflows fail differently than workflows you trigger manually and watch. Nobody is looking at the screen when the API times out at 3 AM. A few patterns make n8n scheduled jobs debuggable after the fact instead of mysterious.
Turn on execution logging and keep enough history. In n8n's workflow settings, set "Save successful executions" and "Save failed executions" to Yes, and check EXECUTIONS_DATA_MAX_AGE (or the equivalent UI retention setting) is long enough that you can actually go look at last Tuesday's run. On instances with heavy scheduled traffic, trimming successful execution data aggressively while keeping failed executions longer is a reasonable default, since failures are what you need to investigate.
Wrap the AI call in error handling, not just retries. The HTTP Request node has a built-in retry setting (Retry On Fail, with configurable retry count and wait time), which handles transient network blips and rate-limit responses. But retries alone will happily retry a malformed prompt three times and still fail. Pair retries with an error branch:
HTTP Request (AI call, retry on fail: 3x)
-> [success] -> continue workflow
-> [error output] -> IF node (check error type)
-> rate limit -> Wait node (60s) -> retry manually
-> other -> Slack/email alert to a monitoring channel, then stopEnable "Continue On Fail" combined with an error output branch (n8n exposes a separate error output on nodes when this is on) rather than letting one bad API response kill the entire scheduled run silently.
Validate the AI response before trusting it downstream. LLM output is not guaranteed to be well-formed, especially if you asked for JSON. A Code node that checks shape before the next step saves you from a scheduled job that posts undefined to Slack every morning:
const raw = $input.first().json.summary;
if (!raw || raw.trim().length < 10) {
throw new Error("AI response too short or empty, aborting digest post");
}
return [{ json: { summary: raw.trim() } }];Throwing here, combined with a workflow-level error trigger (a separate workflow set as the "Error Workflow" in settings, which fires whenever any run in the parent workflow fails), gets you a notification instead of a quiet no-op.
Test with the manual trigger before trusting the schedule. Every Schedule Trigger node can be executed manually from the editor for testing, so validate the full path, fetch, prompt, AI call, parse, post, by hand before you let cron fire it unattended. This catches credential and expression errors early, where they are easy to fix, instead of at 3 AM.
Controlling AI API costs in n8n scheduled jobs
A workflow that runs once when you click it costs one API call to debug. A workflow scheduled every 15 minutes costs that same API call ninety-six times a day, whether or not there was anything new to summarize. Cost control on n8n scheduled jobs is mostly about not calling the model when you do not need to.
Guard the AI call with a cheap pre-check. Before spending tokens, ask whether there is new data at all:
const items = $input.first().json.items || [];
if (items.length === 0) {
// No new data this run, skip the AI call entirely
return [];
}
return [{ json: { items } }];Returning an empty array from a Code node stops execution down that branch, no downstream nodes run, no AI call happens. This single check is usually the biggest cost lever on a frequent schedule.
Cap `max_tokens` deliberately. It is tempting to leave the response length generous "just in case." For a fixed-format digest or classification task, a tight max_tokens value both bounds cost per call and forces the model toward the concise output you actually want.
Batch instead of firing per-item. If your scheduled job processes a list, one AI call summarizing the whole batch is cheaper and faster than a Split In Batches node calling the model once per item. Reserve per-item AI calls for tasks that genuinely need per-item judgment, like classifying each support ticket individually, and batch everything else.
Use IF and Switch nodes to route around the AI call for known-cheap paths. If a rule-based check (keyword match, simple threshold, existing tag) can answer the question without an LLM, put that check before the AI node and only fall through to the model for ambiguous cases. This keeps the scheduled job's AI usage proportional to genuinely uncertain inputs rather than every single run.
Track spend with a lightweight counter. A Code node that logs { runDate, tokensUsed, model } to a spreadsheet node (Google Sheets, Airtable, or a database node) on every AI call gives you a running total without needing a separate observability platform. Most API responses include usage figures in the payload (usage.input_tokens / usage.output_tokens for the Claude API, similarly named fields for other providers), so pull those directly rather than estimating.
Monitoring n8n scheduled jobs in production
Once an n8n scheduled job is doing something real, like posting a report someone reads or triggering a downstream action, you need to know it ran and know it ran correctly, not just assume cron fired.
Heartbeat pattern. Add a final node in every scheduled AI workflow that writes a timestamp somewhere durable, a database row, a Google Sheet cell, or a simple key in an external monitoring service. A separate, unrelated scheduled workflow checks that timestamp on a longer interval and alerts if it is stale:
const lastRun = new Date($json.lastRunTimestamp);
const now = new Date();
const hoursSince = (now - lastRun) / (1000 * 60 * 60);
if (hoursSince > 26) {
// Daily job hasn't run in over a day, something is wrong
throw new Error(`Digest workflow stale: last run ${hoursSince.toFixed(1)}h ago`);
}
return [{ json: { ok: true, hoursSince } }];This catches the failure mode that error branches inside the workflow cannot: the workflow itself failed to trigger at all, because it was deactivated, the n8n instance restarted at the wrong moment, or a queue backed up.
Use the n8n Error Workflow setting, not ad-hoc alerts in every node. Instead of adding a Slack alert node to every possible failure point, set a single dedicated Error Workflow in the workflow's settings panel. It receives execution metadata (workflow name, node that failed, error message) automatically whenever any node throws, and you maintain one alerting workflow instead of duplicating alert logic across every scheduled job.
Watch for workflow deactivation. n8n deactivates a workflow if the owning credential is revoked or, on some setups, after repeated activation errors. Since a deactivated Schedule Trigger simply never fires again, and nothing errors because nothing runs, this is invisible without the heartbeat pattern above. Treat "did it run at all" as a separate concern from "did it run correctly."
Log the AI model and prompt version, not just the output. When you tweak a prompt three months from now and the digest quality changes, you want to be able to correlate that to a specific run. Storing { model, promptVersion, runDate } alongside the output, even in a simple spreadsheet, turns "the summaries got worse in March" into a five-minute investigation instead of a guessing game.
Real-world patterns for scheduled AI workflows
A few shapes come up repeatedly once you start building n8n scheduled jobs around AI calls:
Daily digest. Fetch overnight activity from an API (support tickets, closed PRs, sales leads), summarize with an LLM, post to Slack or email. Schedule at the start of the workday, gate the AI call behind a "any new items?" check.
Content generation pipeline. Pull a topic queue from a database or spreadsheet on a schedule, generate a draft with an LLM, write the draft to a review queue (another spreadsheet row, a CMS draft status) rather than publishing directly. Keep a human approval step outside the scheduled portion.
Anomaly or alert scanning. Poll a metrics or log source every N minutes, only invoke the AI node when a cheap threshold check flags something worth investigating, and use the model to draft a plain-language explanation of the anomaly for the alert message rather than to detect the anomaly itself, which a simple threshold usually does more reliably and far more cheaply.
Data enrichment batch. Nightly job pulls records missing a field (a category, a sentiment label, a summary), processes them in batches through the AI node, writes results back to the source (database node, CRM API, spreadsheet). Use Split In Batches with a reasonable batch size and a Wait node between batches to stay under rate limits on large backlogs.
Recurring report with memory. Some scheduled jobs need to know what happened last time, a weekly trend report comparing this week to last, for instance. Store the previous run's output (a database row, a Sheet, n8n's own static data via the workflow's $getWorkflowStaticData function) and pass it into the prompt as context for a comparison, rather than re-deriving history from scratch every run.
Each of these is the same skeleton: Schedule Trigger, cheap pre-check, AI call with retries and validation, output step, and a monitoring path outside the happy path. The AI node is rarely the hard part. Making the schedule trustworthy when nobody is watching is.
FAQ
How do I stop an n8n scheduled job from running twice if the instance restarts? Self-hosted n8n instances running in "regular" mode track active Schedule Triggers in memory and re-register them on startup based on the workflow's active state, so a restart should not cause a duplicate fire for that exact scheduled time, only resume future scheduling. If you run n8n in queue mode with multiple workers, make sure only one instance owns trigger registration (this is handled by n8n's internal locking in queue mode) to avoid the same schedule firing from more than one worker. When in doubt, add an idempotency check, a workflow static data flag or a database row keyed by the scheduled date, before the side-effecting step (posting, writing, sending).
Can I schedule an n8n workflow to run only on business days? Yes, use a cron expression with a weekday field, 0 8 * * 1-5 runs at 8 AM Monday through Friday. For holidays or more complex business-calendar logic, add an IF node right after the trigger that checks the current date against a maintained list (a Sheet or database table of holiday dates) and stops the workflow early on non-business days.
What happens if the AI API call in a scheduled job times out? The HTTP Request node's own timeout setting (and n8n's execution timeout at the instance level, EXECUTIONS_TIMEOUT for self-hosted setups) will end the node or the run. Configure Retry On Fail on the HTTP Request node so transient timeouts get a second attempt automatically, and set the node timeout generously enough for the model and prompt length you are using, since long generations can legitimately take longer than a typical webhook call.
Should I use n8n's native AI nodes or a generic HTTP Request node for scheduled jobs? Native nodes (the Anthropic node, OpenAI node, and similar) are faster to set up and handle authentication and response parsing for you, which is usually the right default. Reach for a generic HTTP Request node when you need a parameter, model, or endpoint the native node has not exposed yet, or when you want one workflow to be trivially portable across providers by swapping a credential and a URL rather than swapping node types.
How do I test a scheduled workflow without waiting for the cron time to hit? Open the workflow in the editor and use the manual "Execute Workflow" trigger on the Schedule Trigger node itself, or temporarily connect a Manual Trigger node in parallel while building. Both let you run the exact same downstream logic on demand, so you can validate the AI call and error paths before trusting the schedule to fire correctly on its own.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.