Let your agent schedule its own follow-ups
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
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 tableAs 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
| Problem | Do 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 off | The task has no timezone; cron then runs in UTC. |
| A firing never arrives | It was skipped: the sandbox stayed busy, or the task awaited an answer or was archived. |
409 You already own 100 automations | Delete stale follow-ups; a fired one-off still counts until deleted. |
| A push notification when it fires | Give the agent a tool that calls your push API (Connect an MCP server). |