- Why Build a Timer for an Agent?
- Usage
- The Architecture
- The Shell Trick That Makes It Work
- Why This Matters for AI Agents
- Where This Goes Next
- Appendix
AI Agents are often limited to triggers by humans. This is a big limitation. I wanted my AI Agents to be able to schedule work for their future selves. Not Cron. Not a waiting loop. A proper “Hey, wake me up in 20 minutes and remind me what I was thinking” kind of timer. So I built a small workflow that acts as a self-activating timer. Any AI Agent can call it, hand over an interval and a prompt, and then, after the timer expires, the agent is pinged again with precisely the context it left behind. It turns out to be both incredibly simple and incredibly useful.
Why Build a Timer for an Agent?
When you design multi-step automations involving LLMs, you inevitably run into situations where the agent needs to:
- Revisit an idea later
- Perform a follow-up action
- Wait for an external condition to settle
- Or simply re-activate itself to continue a thought process
These aren’t Cron-jobs. They’re dynamic, contextual, and initiated by the agent itself.
For example:
- “Check this inbox again in 10 minutes.”
- “Re-run the analysis tomorrow morning.”
- “Remind me what I was doing when the API rate limit resets.”
- “Wake up later and continue the reasoning chain.”
- “Remind me on the weekend to check xyz.”
Usage
The workflow using this timer needs two items:
- It needs an incoming “When Executed by Another Workflow” trigger, so that the workflow can be triggered when the timer expires.
- The timer needs to be called (e.g., as a tool for an AI Agent). There, the workflow ID of the calling workflow is required (use ``), all other inputs can be defined by the model.
- (optional) If you want to use the timer as a tool, here is an example description:
Call this tool to set a timer by setting the interval. If you require a time for a fixed time, calculate the difference between the current time and your target time and use the result as interval.
You will be triggered when the timer expires and will receive the "timer name" parameter to know which timer has expired, so use an ID for this field. Additionally, you may provide a prompt which will be be included in the triggering prompt for you, once the timer expires. This allows you to carry forward information and instructions, e.g., for recurring timers.
You can find an example workflow JSON with trigger and tool to import below.
The Architecture
The entire workflow is small, but each node does something important. Here’s the high-level overview:
-
Execute Workflow Trigger Another workflow (typically the agent) calls this one, passing:
interval(in seconds)timer name(for reference, allows multiple timers)workflow id(of the agent to re-trigger)prompt(the message it should receive later)
-
Set Timer (Execute Command) This node uses a
setsidshell command to spawn an async background process that:- waits for the given interval
- sends a POST request back into the n8n timer workflow via webhook
This works beautifully because it doesn’t block n8n at all. The timer lives outside the workflow engine, so even long waits don’t consume resources.
-
Timer Webhook When the timer expires, the webhook receives its payload and triggers the second part of the workflow.
-
Assemble Prompt A small text block prepares a clean message for the agent:
This is your trigger that timer X has expired. You have provided the following prompt for yourself: … -
Call Agent (Execute Workflow) Finally, the workflow that originally scheduled the timer is called again, receiving exactly the context it scheduled earlier.
The agent wakes up—with memory.
The Shell Trick That Makes It Work
The heart of the timer is this tiny command:
setsid sh -c 'sleep && wget --header="Content-Type: application/json" --post-data="{\"workflow\":\"\", \"timer name\":\"\", \"prompt\":\"\"}" -qO- http://localhost:5678/webhook/timer &> /dev/null' </dev/null >/dev/null 2>&1 &
This command:
- Detaches itself from the workflow (
setsid) - Sleeps for as long as needed
- Fires a POST to your n8n instance
- Leaves absolutely nothing hanging
It’s crude but works well enough for short timers. Note that there are some limitations:
- When n8n reboots (e.g., during an update), the timer is canceled.
- Most models don’t do well with recursion (e.g., regular messages during countdown timer).
- If you specify a target date/time rather than an interval, the model requires the current date/time to calculate the interval.
- Timers can’t be canceled.
Why This Matters for AI Agents
With this little workflow, agents gain something they normally lack:
- Deferred execution
- Self-scheduling
- Autonomous follow-up
- Lightweight, non-blocking timers
- A cheap approximation of “memory”
And because the agent sets the prompt for its future self, the callback is always meaningful. It’s like leaving a Post-it note for your own automation.
Where This Goes Next
This tool is already surprisingly useful in my setup. But ideas for improvements are piling up:
- Reliable recurring timers
- Timer cancellation
- Time-based prompts (“Every day at 11:00…”)
- An API/dashboard to track which agents are “asleep”
As soon as agents can schedule their own future steps, entirely new patterns emerge. They stop being reactive and start behaving more like colleagues managing their own time.
And all of that begins with a small workflow that just waits, and then knocks on the door again. If you try this out or adapt it for your own automations, I’d love to hear what you build with it.
Appendix
Complete timer workflow
{
"name": "timer",
"nodes": [
{
"parameters": {
"workflowInputs": {
"values": [
{
"name": "interval",
"type": "number"
},
{
"name": "timer name"
},
{
"name": "workflow id"
},
{
"name": "prompt"
}
]
}
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
-144
],
"id": "3a8703b0-c934-47fb-82f5-fc027155d484",
"name": "When Executed by Another Workflow"
},
{
"parameters": {
"httpMethod": "POST",
"path": "timer",
"options": {}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
0,
80
],
"id": "bdad87d6-01d8-48c0-8ed2-cefa42824e69",
"name": "Timer elapsed",
"webhookId": "ec7496f3-bccc-4721-a679-fe6104b6ae9b"
},
{
"parameters": {
"command": "=setsid sh -c 'sleep && wget --header=\"Content-Type: application/json\" --post-data=\"{\\\"workflow\\\":\\\"\\\", \\\"timer name\\\":\\\"\\\", \\\"prompt\\\":\\\"\\\"}\" -qO- http://localhost:5678/webhook/timer &> /dev/null' </dev/null >/dev/null 2>&1 &"
},
"type": "n8n-nodes-base.executeCommand",
"typeVersion": 1,
"position": [
224,
-144
],
"id": "11d25697-5b3a-495a-aa70-6bc3692e6c96",
"name": "Set timer"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "bd8b3a93-9292-4f24-a285-e0c58c6d94ce",
"name": "body.text",
"value": "=This is your trigger that timer has expired.\n\nYou have provided the following prompt for yourself: ",
"type": "string"
}
]
},
"includeOtherFields": true,
"options": {}
},
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
224,
80
],
"id": "fcc0f5b8-caec-47e5-a160-a654b84ab7b3",
"name": "Assemble prompt"
},
{
"parameters": {
"workflowId": {
"__rl": true,
"mode": "id",
"value": "=",
"cachedResultName": "="
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {},
"matchingColumns": [],
"schema": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {
"waitForSubWorkflow": true
}
},
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"position": [
448,
80
],
"id": "5733954f-c934-47df-a8bb-49a2e78bb913",
"name": "Call agent"
}
],
"connections": {
"Timer elapsed": {
"main": [
[
{
"node": "Assemble prompt",
"type": "main",
"index": 0
}
]
]
},
"When Executed by Another Workflow": {
"main": [
[
{
"node": "Set timer",
"type": "main",
"index": 0
}
]
]
},
"Assemble prompt": {
"main": [
[
{
"node": "Call agent",
"type": "main",
"index": 0
}
]
]
}
},
"active": true,
"settings": {
"executionOrder": "v1"
},
"tags": []
}
Trigger and Tool
{
"name": "ParentWorkflow",
"nodes": [
{
"parameters": {
"description": "=Call this tool to set a timer by setting the interval. If you require a time for a fixed time, calculate the difference between the current time and your target time and use the result as interval.\n\nYou will be triggered when the timer expires and will receive the \"timer name\" parameter to know which timer has expired, so use an ID for this field. Additionally, you may provide a prompt which will be be included in the triggering prompt for you, once the timer expires. This allows you to carry forward information and instructions, e.g., for recurring timers.",
"workflowId": {
"__rl": true,
"value": "TimerWorkflowID",
"mode": "list",
"cachedResultUrl": "/workflow/TimerWorkflowID",
"cachedResultName": "timer"
},
"workflowInputs": {
"mappingMode": "defineBelow",
"value": {
"interval": "=",
"timer name": "=",
"prompt": "=",
"workflow id": "="
},
"matchingColumns": [],
"schema": [
{
"id": "interval",
"displayName": "interval",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "number",
"removed": false
},
{
"id": "timer name",
"displayName": "timer name",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "workflow id",
"displayName": "workflow id",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
},
{
"id": "prompt",
"displayName": "prompt",
"required": false,
"defaultMatch": false,
"display": true,
"canBeUsedToMatch": true,
"type": "string",
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
}
},
"type": "@n8n/n8n-nodes-langchain.toolWorkflow",
"typeVersion": 2.2,
"position": [
464,
224
],
"id": "07ddb4a3-2ded-47ef-ad15-7887840859d9",
"name": "timer tool"
},
{
"parameters": {
"inputSource": "passthrough"
},
"type": "n8n-nodes-base.executeWorkflowTrigger",
"typeVersion": 1.1,
"position": [
0,
29
],
"id": "97c0ca7a-6ed4-495c-949b-1ff7ba527bec",
"name": "When Executed by Another Workflow"
}
],
"pinData": {},
"connections": {},
"active": false,
"settings": {
"executionOrder": "v1"
},
"tags": []
}