Tailored AI

Cron jobs

A cron job runs an agent on a schedule without a user in front of it. The scheduler starts when you run tai (alongside the Discord bot and HTTP API). Each job has a schedule, a prompt, an agent to run as, and a delivery channel for the result.

If you want a one-shot "wake the agent and have it do X," cron is the right tool. If you want multi-step processing with conditions or parallel fan-out, you want a Workflow with a cron trigger.

Minimal example

yaml
cron:
  enabled: true
  jobs:
    - name: morning-briefing
      schedule: "0 8 * * 1-5"     # weekdays at 8 AM
      agent: planner
      prompt: |
        Give me a morning briefing: today's calendar, in-progress tasks,
        and one suggestion for what to prioritise.
      delivery:
        channel: discord-dm

Run tai and the scheduler picks it up.

Schedule syntax

Standard 5-field cron (min hour day-of-month month day-of-week). Common patterns:

ExpressionWhen
*/15 * * * *every 15 minutes
0 * * * *top of every hour
0 8 * * *daily at 8 AM
0 8 * * 1-5weekdays at 8 AM
0 0 1 * *first of every month at midnight

The scheduler is croner. See its docs for advanced expressions.

Job options

yaml
- name: my-job
  schedule: "0 * * * *"
  agent: default              # which agent; falls back to `default`
  prompt: "Do the thing."     # the user-message equivalent
  wakeAgent: true             # default; false = inject prompt without running loop
  newSession: false           # default; true = fresh session each run
  hooks: {}                   # job-specific hooks (appended to agent's hooks)
  delivery:
    channel: log              # log | discord | discord-dm | email
    target: "<channel-id>"    # required for channel: discord
  enabled: true               # set false to keep the job in config but skip it

wakeAgent: false is the "inject context without thinking" mode. The prompt becomes a user message in the agent's session without running the loop. Useful for preloading the next conversation with a fact note, without burning LLM tokens.

newSession: true forces a fresh session each run. Without it, the cron job's conversation history accumulates over time.

Delivery channels

ChannelBehaviour
logPrints to stdout / the agent log. Right for jobs whose side effect is the point (the file already exists; delivery is just confirmation).
discordPosts to a Discord channel. Requires delivery.target (the channel id).
discord-dmDMs the configured channels.discord.owner.
emailSends via the gmail tool. Requires Gmail wired up and the agent's tool list to include gmail.

For unsolicited push from an agent: prefer discord-dm over email unless the user explicitly wants it in their inbox.

Prompt templates

Cron prompts substitute these variables:

VariableSource
{{last_run}}Local time of the previous run, formatted.
{{last_run_epoch}}Unix timestamp of the previous run.
{{last_run_iso}}ISO 8601 of the previous run.
{{last_response}}The agent's full response from the previous run.
{{next_task}}The first Next: … line from the agent's goals.md.
{{now}}Current ISO timestamp.

{{next_task}} is useful for "do whatever I told you to do next" patterns:

yaml
- name: autonomous-explore
  schedule: "0 */4 * * *"
  agent: autonomous
  prompt: "Your task: {{next_task}}"
  newSession: true
  delivery: { channel: log }

The goals.md lives at data/context/agents/<agent-name>/goals.md. You write a list of Next: … lines; the agent picks the top one each run.

Job-specific hooks

Cron jobs can append hooks to the agent's normal hooks:

yaml
- name: bill-alert
  schedule: "0 9 * * *"
  agent: default
  prompt: |
    Check for new bills and flag anything due in the next 7 days.
    Hook context: {{hookContext}}
  hooks:
    beforeRun:
      - tool: gmail
        args:
          action: search
          query: "subject:(invoice OR bill OR \"due\")"
          exclude_seen: true
        skipIf: "^No results"

skipIf is the kill switch: if the hook output matches, the whole job (agent loop plus delivery) is skipped. Saves LLM cycles when the trigger condition hasn't fired.

See Hooks for the full hook surface.

State persistence

Job state (last_run_at, last_response) persists to the cron_runs table in agent.db. The {{last_*}} template variables read from there. State survives restarts, so last_run_epoch is genuinely the last actual run, not the last run of the current process.

Cron vs workflows vs online mode

WantUse
Fire one prompt on a scheduleCron job
Fire on an event (new file, new email, webhook)Workflow with the matching trigger
Multi-step pipeline, scheduled or triggeredWorkflow
Agent that thinks on its own cadence and picks what to doOnline mode on an agent

You can combine them: a cron job that triggers a workflow, a workflow whose output gets injected into a different cron job's hook context, an online agent that creates cron jobs.

Inspecting runs

The web UI's Cron page shows configured jobs and recent runs with status, duration, and response preview. CLI surface for cron (tai cron list, tai cron run <name>) is on the roadmap; for now use the web UI or query cron_runs directly.