# Giving Your Neovim AI Assistant a Sense of Time: Scheduled Tasks in chat.nvim

> Source: <https://dev.to/wsdjeg/giving-your-neovim-ai-assistant-a-sense-of-time-scheduled-tasks-in-chatnvim-3da9>
> Published: 2026-07-08 13:27:59+00:00

You tell AI "fix this code," it responds instantly. You say "remind me about the meeting in an hour," it says "sure" — and then nothing happens.

This isn't a bug in one product. It's a common limitation across most mainstream AI assistants: **they are passive.** They answer when you ask. They stay silent when you don't. They have no concept of "time," no concept of "the future," and no ability to initiate anything on their own. While some open-source or closed-source Agent frameworks already support scheduled tasks, mainstream assistants like Doubao and DeepSeek still lack this capability.

[chat.nvim](https://github.com/wsdjeg/chat.nvim) now includes a scheduled task feature, designed to give AI a sense of time — letting it proactively reach out to you at a future moment.

To break AI's passivity, the key question is: **how do we make AI start working at a future moment, as if it just received a user message?**

The solution is straightforward, three steps:

Simple in concept, but the devil is in the details. Let's walk through the design decisions for each part.

Users can describe time in three ways:

Pick one, they're mutually exclusive.

Regardless of how the user describes it, internally everything converts to a Unix timestamp. "In 1 hour" becomes `current_time + 3600`

. "Every day" becomes every 86400 seconds starting from creation time.

Why? Because the scheduling engine only needs to handle one time model. No need to distinguish between "delay" and "schedule," no need for different logic branches. Unified abstraction eliminates branching.

The most intuitive approach is polling: scan the task list every few seconds to check if anything is due.

That's wasteful. 99 out of 100 tasks are still waiting, yet you scan them every second.

A better approach: one independent timer per task. It wakes up only when due, with zero CPU overhead while waiting. This is event-driven, not poll-driven.

Neovim has libuv built in. `uv.new_timer()`

does exactly this — millisecond precision, non-blocking on the main thread.

Recurring tasks have a pitfall: if Neovim restarts midway, how do you calculate the next trigger time after recovery?

The instinctive approach is `current_time + interval`

. But this causes rhythm drift — you set "every day at 10 AM," after a restart it becomes 10:03, after another restart 10:07...

The correct approach: calculate the next trigger based on creation time and execution count.

```
next_trigger = creation_time + (executed_count + 1) × interval
```

This way, no matter how many restarts happen, the 7th execution always lands at `creation_time + 7 × interval`

. The rhythm is locked to the moment of creation.

libuv timers have a hard limit: maximum timeout of `2³¹-1`

milliseconds, approximately 24.8 days.

What if a user sets a task 30 days out?

The solution is sharding. First set a 24.8-day timer. When it fires, check — the real trigger time hasn't arrived yet, so recalculate the remaining delay and set a new timer. Two relays cover any duration.

The 30-day cap covers reasonable use cases while preventing a buggy task from hanging around forever.

This is the most critical decision in the entire design.

The scheduling engine does exactly one thing: when the time comes, push a message into the queue. It doesn't call the LLM API, doesn't care if AI is online, doesn't care about network status.

Why not just call the API directly when the timer fires? Because once responsibilities are mixed, complexity explodes:

After decoupling, the scheduling engine's logic is dead simple: fire → push → done. Everything else is handled by the message queue.

After a task fires, the message enters a queue. The queue is responsible for delivering the message to AI at "the right moment."

What's the right moment? **When the session is idle.**

It's like a colleague messaging you while you're in a meeting — the message doesn't disappear, you just read it after the meeting.

Only after 3 consecutive send failures does the message get discarded, preventing transient glitches from swallowing messages.

Neovim isn't a persistent process — users can close it at any time. Scheduled tasks must survive restarts.

The approach is straightforward: every time a task is created, cancelled, or fired, write all tasks to a JSON file on disk. On next launch, load it back.

One detail: libuv timers are in-memory objects that can't be serialized. When persisting, the timer handle must be excluded and recreated on recovery.

Recovery has two different strategies:

```
You say: "Remind me about the meeting in 1 hour"
    │
    ▼
AI calls the tool, calculates trigger_at = current_time + 3600
    │
    ▼
Scheduling engine creates the task, writes to disk, arms a 3600s timer
    │
    ··· (1 hour later) ···
    │
    ▼
Timer fires → pushes "Remind me about the meeting" into the session's message queue
    │
    ▼
Queue detects session is idle → sends to AI
    │
    ▼
AI receives the message, responds with a reminder — just as if you said it yourself
```

Just tell AI in natural language:

Remind me to follow up on the Subei People's Hospital project in 1 hour

AI will automatically calculate the time and create a scheduled task.

Three modes:

`delay_seconds=3600`

or `trigger_at=1717200000`

`interval=86400`

(daily), optionally `repeat_count=7`

to limit repeats`action="list"`

to view, `action="cancel"`

to cancel| Feature | Details |
|---|---|
| Persistence | Survives Neovim restarts |
| Max delay | 30 days |
| Min interval | 10 seconds |
| Precision | Second-level (Unix timestamp based) |
| Dependencies | Zero external deps — pure Neovim + libuv |

The scheduled task system in chat.nvim proves that giving AI a sense of time doesn't require a complex architecture. The key design principles:

The result: AI that can proactively reach out when you need it, not just respond when you ask.
