Skip to Content
Platform
How-toLet the agent schedule follow-ups

Let your agent schedule its own follow-ups

The agent wakes itself up: a daily heartbeat, and a next step at the time it chose.

An agent follow-up is an automation the agent creates in its own conversation; each firing arrives as a new turn.

You need: end users signed in through an application (Max or Enterprise plan) and a recipe you can change (Add a tool). For other tasks, schedule from outside.

1. Allow the scopes on the application

--allowed-scope replaces the list, so repeat the existing scopes.

introspection applications get <application-id> --query allowed_scopes introspection applications update <application-id> \ --allowed-scope "<existing scopes> automations:read automations:write"

2. Keep the user’s time zone on the task

await runner.tasks.start({ prompt, metadata: { timezone: "Europe/London" } });

3. Add the scheduling tool

extensions/follow-ups.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; const env = process.env; const api = async (method: string, path: string, body?: unknown) => { const res = await fetch(`${env.INTROSPECTION_BASE_API_URL}${path}`, { method, headers: { Authorization: `Bearer ${env.INTROSPECTION_TOKEN}`, "Content-Type": "application/json" }, body: body && JSON.stringify(body), }); if (!res.ok) throw new Error(`follow-up refused (${res.status}): ${await res.text()}`); return res.status === 204 ? null : res.json(); }; const timezone = JSON.parse(env.INTROSPECTION_TASK_METADATA_JSON ?? "{}").timezone; export default function (pi: ExtensionAPI) { pi.registerTool({ name: "follow_up", label: "Follow up", description: "Schedule a turn in this conversation: cron (repeating, user's zone) or at (ISO time, once). Pass id to move a one-off.", parameters: Type.Object({ id: Type.Optional(Type.String()), title: Type.String(), prompt: Type.String({ description: "Self-contained: it arrives as a new message." }), cron: Type.Optional(Type.String()), at: Type.Optional(Type.String()), }), async execute(_callId, p) { const saved = p.id ? await api("PATCH", `/v1/automations/${p.id}`, { next_trigger_at: p.at, prompt: p.prompt }) : await api("POST", "/v1/automations", { name: p.title, prompt: p.prompt, task_id: env.INTROSPECTION_TASK_ID, runtime_group_id: env.INTROSPECTION_RUNTIME_GROUP_ID, ...(p.cron ? { trigger_type: "cron", cron_schedule: p.cron, metadata: { cron_schedules: [p.cron], timezone } } : { trigger_type: "manual", next_trigger_at: p.at }), }); return { content: [{ type: "text", text: `${saved.id} next ${saved.next_trigger_at}` }], details: {} }; }, }); }

Add follow_up to the agent’s tools. Add list (GET /v1/automations?task_id=) and cancel (DELETE /v1/automations/<id>) the same way.

4. Tell the agent how to use it

In SYSTEM.md, ask for one heartbeat cron (such as 0 8 * * *) created on the first turn, one at for the next due to-do (moved by id as to-dos change), and one at per reminder the user asks for. Prompts must make sense whenever they arrive.

Verify

introspection automations list -o table

As a project admin, expect the follow-ups with the task’s task_id, and a new turn when each fires.

Or ask your agent: “Give support-agent a follow_up tool with a daily heartbeat.”

Limits

ProblemDo this
403 Missing required scope: automations:write or Only a member's own conversation session…The application lacks the scopes (step 1), or the task isn’t a signed-in user’s: schedule from outside.
Fires an hour offThe task has no timezone; cron then runs in UTC.
A firing never arrivesIt was skipped: the sandbox stayed busy, or the task awaited an answer or was archived.
409 You already own 100 automationsDelete stale follow-ups; a fired one-off still counts until deleted.
A push notification when it firesGive the agent a tool that calls your push API (Connect an MCP server).
Last updated on