7 min read · 2026-05-08
How Vex's autonomous loops work
Vex isn't a chat box you have to prompt. It runs scheduled goals while you sleep — fetches your daily brief, triages email, summarises overnight news. Here's how the autonomous loop is actually wired.
The vex-scheduler
The Vex daemon runs a scheduler that polls a SQLite table called nebula_goals every 30 seconds. Each row is a goal: a user, a prompt, a schedule (one-shot, every-N-seconds, or cron), a next_run_at timestamp, and the last outcome.
-- Schema (simplified) CREATE TABLE nebula_goals ( id TEXT PRIMARY KEY, user_id TEXT NOT NULL, description TEXT NOT NULL, status TEXT NOT NULL, -- active | paused | completed | failed prompt TEXT NOT NULL, schedule_kind TEXT NOT NULL, -- one_shot | every_n_seconds | cron schedule_value TEXT, next_run_at TEXT, last_outcome TEXT );
When next_run_at is in the past and status is active, the scheduler fires runVexAgent with the prompt and the user's id. Whatever Vex outputs gets saved as last_reply; next_run_at ticks forward by schedule_valueseconds.
The daily-brief example
Mine fires at 07:00 UTC every morning:
{
description: "Make today's daily_brief PDF and chat-message me",
prompt: "Make today's daily_brief PDF, then send a chat message
saying 'Good morning ☀️' with the chat_link.",
schedule_kind: "every_n_seconds",
schedule_value: "86400",
next_run_at: "2026-05-08T07:00:00+00:00",
status: "active"
}When 07:00 UTC hits, Vex orchestrates four things:
daily_brieftool combines RSS headlines + weather into a markdown brief.make_pdfrenders it via pandoc + weasyprint.- The PDF is uploaded to my Supabase Storage bucket and registered in
files. pulse_send_chat_messagedrops a message in my "Vex autonomous" chat thread with a clickable link.
When I open Pulse, there's a fresh thread waiting with the brief attached. This is the difference between an AI assistant and an AI agent. The assistant waits for me. The agent does work.
UUID hygiene
The scheduler has a daemon-internal user ID convention ("raven") that isn't a real Supabase UUID. When autonomous tools need to talk to the database (file register, chat insert), they FK-fail on the string ID. The fix is a small resolveRealUserId() helper that maps "raven" to the actual auth.users UUID via the email lookup. Tools that need DB writes call it; tools that only do filesystem ops don't bother.
What can go wrong
- Duplicate goals. If you accidentally insert the same goal twice (we did), both fire and the user gets two daily briefs. We caught and paused one. Add a
UNIQUEconstraint on (user_id, prompt, schedule_kind, schedule_value) to make this impossible. - Stuck goals. If
next_run_atis set but the daemon crashed mid-fire, the goal can become a tight loop on restart. Caplast_run_at - next_run_atdrift to one interval; skip stale fires. - Quiet failures. If the LLM errors mid-goal, you lose the artifact. The graceful-recovery layer in the agent loop surfaces partial work (PDF written but chat message failed → still save the PDF and notify next time).
Adding a new autonomous goal
You don't write code — you POST a row. Through the API, the user can schedule "summarise my unread emails every hour" or "post Norwegian market open headlines at 09:00 weekdays". The agent picks the right tools (mail-fetch, summarize, send-chat) automatically because Vex already has them.
Why this is rare
ChatGPT can't do this — it has no scheduler, no database, no persistent file system. Claude can't do this without a separate agent harness. Most "AI agents" are vendor-locked stacks (ZAPier Central, etc) where the LLM and the scheduler are different products.
Pulse ships them in one box, on your hardware. Your data never leaves your network, and your agent runs whether or not you're at the keyboard.
Try it
Self-host Pulse. Set a daily brief at the time you wake up. Add an email digest at the time you sit down. Wake up to a workspace that's been working.
Create your workspace