ChatGPT MCP Events: Start Work Tasks From Gmail, Slack, GitHub

TL;DR
How to start ChatGPT Work tasks from Gmail, Slack, GitHub and your own MCP server events: setup steps, the 30-runs-per-hour cap and signed webhook code.
ChatGPT Work can now start a task when something happens in a connected app, not only on a clock. It shipped in two layers: on August 25, 2026 OpenAI added first-party webhook triggers for new Gmail messages, Slack channel messages and GitHub pull request activity, and on September 29 it opened the same idea to any plugin whose MCP server implements MCP Events (events/list, events/subscribe, events/unsubscribe and signed webhook delivery). Our verdict: for users it is the simplest way to get an agent that reacts instead of re-checking on a timer, and for plugin builders it is the first mainstream client of the draft MCP Events spec - worth implementing now if your product has events users care about, with two caveats: delivery is webhook-only, and the spec is still renumbering its error codes.
Last updated: October 17, 2026
What shipped, and when#
Event triggers are the latest step in a feature that started as reminders. If you want the background on how scheduled tasks began, our original ChatGPT Tasks explainer covers the first version. The 2026 timeline, from the ChatGPT release notes:
| Date | What changed |
|---|---|
| June 17, 2026 | New Scheduled page in the sidebar to view, pause, resume, edit or delete tasks. Tasks cannot run more than once per hour; unattended tasks may auto-pause. |
| July 9, 2026 | ChatGPT Work launches. Work can run scheduled tasks "that run once, repeat on a schedule or trigger, or monitor for changes." |
| August 25, 2026 | Work scheduled tasks can use webhooks for new Gmail messages, Slack channel messages and GitHub pull request activity. Scheduled tasks become shareable. Free users get up to three active scheduled tasks. |
| September 29, 2026 | "Start automations from connected-app events": supported plugins can use MCP events to start an automation when something changes in a connected app. |
It helps to keep the two layers separate in your head:
- First-party event triggers (Gmail, Slack, GitHub). You create them from a Work chat, review a Trigger, Condition and Prompt, and manage them on the Scheduled page like any other task.
- MCP-event subscriptions (any plugin that ships events). Per OpenAI's MCP Events developer doc, ChatGPT subscribes through the plugin's MCP server from a Work chat or a dot, and events arrive "in the subscribed chat." OpenAI does not currently document whether these subscriptions also appear on the Scheduled page, so we will not guess.
Why it matters to developers#
1. Polling becomes push. Before this, the closest option was a monitoring task that re-checked on a cadence, with a floor of once per hour on paid plans. Event-triggered tasks run when the change happens. They are still capped: up to 30 runs per hour and 720 per day across all of a user's event-triggered tasks, and ChatGPT may group several matching events into one run, per the Scheduled tasks help article.
2. Your product can become a trigger source without OpenAI building an integration. Gmail, Slack and GitHub got first-party treatment. Everything else gets in through MCP Events: add three methods and a signed webhook sender to your plugin's MCP server, and users can say "watch this queue and do X when a ticket lands" in plain language. The user owns the instructions; you own the event catalog and the filters.
3. It is built on an open draft, not a private API. Delivery uses Standard Webhooks signing with whsec_ secrets, and the method names come from the draft MCP Events design sketch. In principle the same server work can serve other MCP clients that adopt the extension. In practice the draft is still moving, which we cover under Gotchas.
%% title: Which ChatGPT trigger should you use
flowchart TD
A[Need recurring work] --> B{Fires on a change?}
B -- No --> C[Time-based scheduled task]
B -- Yes --> D{Gmail, Slack or GitHub PR?}
D -- Yes --> E[First-party event trigger in Work]
D -- No --> F{App ships MCP events?}
F -- Yes --> G[Subscribe from a Work chat or dot]
F -- No --> H[Build MCP events in your server]
C --> I[Scheduled page]
E --> IHow to use it as a ChatGPT user#
These steps come from OpenAI's Scheduled tasks article and the automations docs. You need an eligible Plus, Pro, Business, Enterprise or Edu plan, or a ChatGPT for Healthcare workspace. Free and Go cannot create event-triggered tasks, and they are not available in FedRAMP workspaces.
-
Connect the app. Go to Settings > Plugins (the connected apps article uses this label; older docs and older mobile builds say Settings > Apps) and connect Gmail, Slack or GitHub with the account that actually has access to the mailbox, channel or repository. The connected account must have access to the event and to anything the task reads.
-
Prepare the source. For Slack, add
@ChatGPTto every channel the task watches. For GitHub, the connected app needs access to the repository, and only github.com repositories are supported. -
Describe the event and the action in Work. Open Work on web or mobile. Trigger creation is not available in the ChatGPT desktop app, the Codex CLI or the IDE extension; the desktop app can show existing event-triggered tasks but cannot create or edit their trigger conditions. A prompt that works well names the source, the filter and the approval boundary:
TextWhen a pull request in acme/webapp gets a review comment, summarize the requested changes and draft a reply. Do not post anything without my approval. -
Review Trigger, Condition and Prompt. ChatGPT shows what it will listen for, the filter it inferred and the instructions it will run. Fix anything that is too broad here, and complete any authorization it asks for.
-
Manage it in Scheduled. The Scheduled page shows pending events and a Run now option, and lets you pause, edit or delete the task.
The filters you can ask for, per app:
| App | Trigger | Filters |
|---|---|---|
| Gmail | New message | Sender, subject |
| Slack | New messages in selected channels | Author, whether thread replies count. Reactions, edits, deletes and DMs are not supported. |
| GitHub | Pull request activity in an authorized repo | PR, author, title, label, and whether reviews, comments, commit updates or only merges trigger a run |
Three behaviors to know before you rely on it. One task can hold several event triggers, but it cannot combine event triggers with a time-based schedule. When several matching events arrive close together, ChatGPT may combine them into one run. And any action that sends a message or changes external data may require approval under your app permissions; when it does, the task pauses until you review it. Installing a plugin or connecting an app does not create a trigger on its own - you always create the task and review its conditions.
Sharing a task, and what the link exposes#
The August 25 release also made tasks shareable for Free, Go, Plus and Pro users. To share, go to Scheduled, open the task's more options menu (...), select Share, review the details in the Share task dialog, then Copy link. Recipients review and customize the instructions and schedule, connect their own apps, and create an independent copy.
What the link carries, per the help article: a snapshot of the task title, instructions, schedule and original time zone, and depending on the task, its selected mode or model. What it does not carry: your name, chat history, previous results, memories, custom instructions, attached files, connected app data or credentials.
The gotchas:
- Anyone with a personal-account link can read the full instructions, and the title may show in link previews. Keep repo names you consider private, customer names and anything sensitive out of titles and prompts. Workspace links open only for members of that workspace.
- Links are snapshots. Editing the original task does not update the link until you open Share again and select Copy link.
- Webhook tasks need more on the receiving end. To schedule a shared event-triggered task, the recipient needs Work access, their own supported app connection and any required workspace permissions.
- Deleting the chat pauses the task but does not delete its share link. Delete the link separately from the Share dialog. Tasks that run only on your computer cannot be scheduled from a shared link.
- Free users get three active scheduled tasks that run once or recur no more than once per day, with flexible windows, and no event triggers.
How to add MCP events to your plugin's MCP server#
This is the part that matters if you build a product. Everything below follows OpenAI's MCP Events doc. Prerequisites:
- MCP protocol version
2026-07-28(OpenAI calls it MCP 2.0). If your server is still on an older revision, start with our MCP 2026-07-28 migration guide. - Persistent subscription storage that survives restarts for the lifetime you grant.
- Outbound HTTPS from your server to ChatGPT's callback URLs.
If you have not built a ChatGPT plugin server before, our Apps SDK walkthrough covers the server that events get added to.
Step 1: advertise event support#
Add events to the capabilities in your server/discover result:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"resultType": "complete",
"supportedVersions": ["2026-07-28"],
"capabilities": {
"tools": {},
"events": {}
}
}
}
Then implement events/list, events/subscribe and events/unsubscribe on the same authenticated MCP endpoint as your tools.
Step 2: define your events#
events/list returns the catalog. inputSchema describes the subscription arguments (your filters), and payloadSchema describes the data object of each delivered event:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"events": [
{
"name": "comment.created",
"description": "A new review comment was added to the specified document.",
"delivery": ["webhook"],
"inputSchema": {
"type": "object",
"properties": {
"document_id": {
"type": "string",
"description": "ID of the document to monitor for new review comments."
}
},
"required": ["document_id"],
"additionalProperties": false
},
"payloadSchema": {
"type": "object",
"properties": {
"document_id": { "type": "string" },
"comment_id": { "type": "string" },
"text": { "type": "string" },
"url": { "type": "string" }
},
"required": ["document_id", "comment_id", "text", "url"],
"additionalProperties": false
}
}
]
}
}
OpenAI asks for stable event names and specific descriptions; the description is what tells ChatGPT which event fits a user's request. Apply filters on your server before delivery, return only events the connected account is allowed to discover, and return nextCursor if the catalog spans multiple pages.
Step 3: accept subscriptions#
When a user asks ChatGPT to watch something, it calls events/subscribe:
{
"jsonrpc": "2.0",
"id": 2,
"method": "events/subscribe",
"params": {
"name": "comment.created",
"arguments": { "document_id": "doc_123" },
"delivery": {
"mode": "webhook",
"url": "https://receiver.example.com/mcp-events/callback_123",
"secret": "whsec_<base64-encoded-signing-key>"
},
"cursor": null
}
}
Before you accept it: authorize the user for that event and those arguments, validate both against your definition, require a whsec_ secret whose base64 value decodes to 24-64 bytes, verify the callback (Step 4) and store the owner, filters, URL, secret and expiry. The response returns a deterministic ID and the expiry you grant:
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"id": "sub_123",
"refreshBefore": "2026-10-02T12:00:00Z",
"cursor": null,
"truncated": false
}
}
Subscribe must be idempotent: the ID is derived from the authenticated principal, callback URL, event name and arguments, and arguments are compared as canonical JSON so key order never creates a duplicate. These helpers cover the three rules that are easy to get subtly wrong:
import { createHash } from "node:crypto";
// A whsec_ secret whose base64 body decodes to 24-64 bytes (OpenAI MCP Events doc).
export function validSecret(secret: unknown): secret is string {
if (typeof secret !== "string" || !secret.startsWith("whsec_")) return false;
const b64 = secret.slice("whsec_".length);
if (!/^[A-Za-z0-9+/]+={0,2}$/.test(b64)) return false;
const bytes = Buffer.from(b64, "base64").length;
return bytes >= 24 && bytes <= 64;
}
// Canonical JSON: sorted object keys, so key order never creates a duplicate.
export function canonicalJson(value: unknown): string {
if (value === null || typeof value !== "object") return JSON.stringify(value);
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`;
const entries = Object.entries(value as Record<string, unknown>)
.filter(([, v]) => v !== undefined)
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalJson(v)}`).join(",")}}`;
}
export interface SubscribeParams {
name: string;
arguments: Record<string, unknown>;
delivery: { mode: "webhook"; url: string; secret: string };
cursor: string | null;
ttlMs?: number | null;
}
// Deterministic ID from principal + callback URL + event name + arguments.
export function subscriptionId(principal: string, p: SubscribeParams): string {
const identity = canonicalJson({
principal,
url: p.delivery.url,
name: p.name,
arguments: p.arguments,
});
return "sub_" + createHash("sha256").update(identity).digest("hex").slice(0, 32);
}
const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; // server default: 24 hours
const MIN_TTL_MS = 5 * 60 * 1000; // floor to avoid refresh storms
// This server never grants ttlMs: null, so refreshBefore is always finite.
export function grantRefreshBefore(ttlMs: number | null | undefined, now = new Date()): string {
const requested = typeof ttlMs === "number" ? ttlMs : DEFAULT_TTL_MS;
const granted = Math.max(MIN_TTL_MS, Math.min(requested, DEFAULT_TTL_MS));
return new Date(now.getTime() + granted).toISOString();
}
The 24-hour default and 5-minute floor are our choices, not OpenAI's. The doc's rule is: when ttlMs is omitted use your server default, grant no more than a requested ttlMs (except to enforce a minimum), and return refreshBefore: null only if you grant a ttlMs: null request for no expiry. This helper simply never grants that.
Step 4: verify the callback#
Before sending application data, POST a signed {"type": "verification", "challenge": "..."} body with a unique webhook-id, plus webhook-timestamp, webhook-signature and X-MCP-Subscription-Id. ChatGPT echoes {"challenge": "..."}. Require a 2xx, compare the challenge in constant time, and on failure return JSON-RPC error -32015 (CallbackEndpointError) with a categorized data.reason such as challenge_failed or timeout:
import { randomBytes, timingSafeEqual } from "node:crypto";
import { Webhook } from "standardwebhooks";
import type { WebhookFetch } from "./send";
export class CallbackEndpointError extends Error {
// OpenAI's ChatGPT doc: JSON-RPC -32015. The draft spec has since renumbered (see Gotchas).
readonly code = -32015;
constructor(readonly reason: "challenge_failed" | "timeout" | "bad_status") {
super(`Callback verification failed: ${reason}`);
}
}
export async function verifyCallback(
subscriptionId: string,
url: string,
secret: string,
webhookFetch: WebhookFetch,
): Promise<void> {
if (!url.startsWith("https://")) throw new CallbackEndpointError("bad_status");
const challenge = randomBytes(24).toString("base64url");
const msgId = `msg_verification_${randomBytes(8).toString("hex")}`;
const body = JSON.stringify({ type: "verification", challenge });
const signedAt = new Date();
let response: Response;
try {
response = await webhookFetch(url, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(10_000),
headers: {
"Content-Type": "application/json",
"webhook-id": msgId,
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
"webhook-signature": new Webhook(secret).sign(msgId, signedAt, body),
"X-MCP-Subscription-Id": subscriptionId,
},
body,
});
} catch {
throw new CallbackEndpointError("timeout");
}
if (!response.ok) throw new CallbackEndpointError("bad_status");
const echoed: unknown = await response.json().catch(() => null);
const got =
echoed && typeof echoed === "object" && "challenge" in echoed
? String((echoed as { challenge: unknown }).challenge)
: "";
const a = Buffer.from(got);
const b = Buffer.from(challenge);
if (a.length !== b.length || !timingSafeEqual(a, b)) {
throw new CallbackEndpointError("challenge_failed");
}
}
bad_status is our own category; challenge_failed and timeout are the examples OpenAI names. Cache a successful verification by principal and callback URL for a bounded period so repeated subscribes do not trigger repeated challenges. The webhookFetch you pass in is where the network rules live: HTTPS only, resolve and validate the destination at connection time, block private, local and other non-public addresses, keep the original hostname for TLS, and never follow redirects. Apply the same checks to verification and to event delivery.
Step 5: send events#
When something matches an active subscription, POST one event object per request:
{
"eventId": "evt_456",
"name": "comment.created",
"timestamp": "2026-10-01T12:05:00Z",
"data": {
"document_id": "doc_123",
"comment_id": "comment_456",
"text": "Can we add the rollout dates to this section?",
"url": "https://docs.example.com/doc_123#comment_456"
},
"cursor": null
}
This is OpenAI's Node.js sendEvent example with TypeScript types added. It serializes the body once (the signature covers the exact bytes), guards the 256 KiB limit, refuses redirects and times out after 10 seconds:
import { Webhook } from "standardwebhooks";
export interface Subscription {
id: string;
url: string;
secret: string; // whsec_...
}
export interface McpEvent {
eventId: string;
name: string;
timestamp: string; // ISO 8601 with timezone
data: Record<string, unknown>;
cursor: string | null;
}
// Same interface as fetch, but validates callback addresses on each connection.
export type WebhookFetch = (url: string, init: RequestInit) => Promise<Response>;
export async function sendEvent(
subscription: Subscription,
event: McpEvent,
webhookFetch: WebhookFetch,
): Promise<{ accepted: boolean; status: number }> {
const body = JSON.stringify(event);
if (Buffer.byteLength(body, "utf8") > 256 * 1024) {
throw new Error("Event payload exceeds 256 KiB");
}
const signedAt = new Date();
const signer = new Webhook(subscription.secret);
const response = await webhookFetch(subscription.url, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(10_000),
headers: {
"Content-Type": "application/json",
"webhook-id": event.eventId,
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
"webhook-signature": signer.sign(event.eventId, signedAt, body),
"X-MCP-Subscription-Id": subscription.id,
},
body,
});
return { accepted: response.ok, status: response.status };
}
| Header | Value |
|---|---|
Content-Type | application/json |
webhook-id | Same value as the body's eventId |
webhook-timestamp | Signing time in Unix seconds |
webhook-signature | Standard Webhooks HMAC signature |
X-MCP-Subscription-Id | The ID you returned from events/subscribe |
A 2xx only acknowledges receipt; ChatGPT processes the event asynchronously. Retry transient failures with exponential backoff and bounded attempts, keeping the same eventId but generating a fresh timestamp and signature each attempt. Do not retry a 410 or 413.
Step 6: refresh, rotate and unsubscribe#
ChatGPT calls events/subscribe again before refreshBefore, with the same identity and the last saved cursor. Update the existing record and return a new expiry. A refresh may carry a replacement secret: store it, and during a short rotation window sign with both keys using Standard Webhooks' space-separated signatures. Recheck the user's access during the subscription's life and stop delivery when it is revoked. events/unsubscribe arrives with the original name, arguments and callback URL; stop delivery, return an empty result: {}, and make it idempotent.
Examples in this section were type-checked against standardwebhooks@1.1.1 with TypeScript 7.0.2 and @types/node 26.6.5 (strict, moduleResolution: bundler), and the local sign and verify round trip below ran on Node 22.22.0. We did not call ChatGPT or any OpenAI API.
%% title: MCP events subscription lifecycle
sequenceDiagram
participant User
participant ChatGPT
participant Server as Your MCP server
participant App as Source app
User->>ChatGPT: Watch doc_123, apply edits
ChatGPT->>Server: events/list
Server-->>ChatGPT: comment.created schema
ChatGPT->>Server: events/subscribe (url, whsec_)
Server->>ChatGPT: Signed verification challenge
ChatGPT-->>Server: 2xx + challenge
Server-->>ChatGPT: id, refreshBefore
App->>Server: New comment
Server->>ChatGPT: Signed event POST
ChatGPT-->>Server: 2xx
ChatGPT->>Server: tools/call (read, write)
Note over ChatGPT,Server: Refresh before refreshBefore%% title: Delivery and retry rules
flowchart LR
E[Matching event] --> S{Body over 256 KiB?}
S -- Yes --> T[Send summary, expose read tool]
S -- No --> G[Sign with whsec_ secret]
G --> P[POST to callback]
P --> R{Response}
R -- 2xx --> D[Done]
R -- 410 or 413 --> X[Stop, no retry]
R -- 5xx or timeout --> B[Backoff, same eventId, new signature]
B --> PTest locally before you connect ChatGPT#
You can prove the signing and idempotency pieces without a ChatGPT account. This script generates a whsec_ secret, checks the length rule, confirms the subscription ID ignores key order, and round-trips a signature through the library's own verifier:
import { randomBytes } from "node:crypto";
import { Webhook } from "standardwebhooks";
import { subscriptionId, validSecret, type SubscribeParams } from "./events";
const secret = "whsec_" + randomBytes(32).toString("base64");
console.log("secret valid:", validSecret(secret));
console.log("short secret valid:", validSecret("whsec_" + randomBytes(8).toString("base64")));
const params: SubscribeParams = {
name: "comment.created",
arguments: { document_id: "doc_123", author: "any" },
delivery: { mode: "webhook", url: "https://receiver.example.com/cb", secret },
cursor: null,
};
// Same arguments, different key order: the ID must not change.
const reordered: SubscribeParams = { ...params, arguments: { author: "any", document_id: "doc_123" } };
console.log("id stable:", subscriptionId("user_1", params) === subscriptionId("user_1", reordered));
const event = {
eventId: "evt_456",
name: "comment.created",
timestamp: new Date().toISOString(),
data: { document_id: "doc_123", comment_id: "comment_456", text: "Add rollout dates", url: "https://docs.example.com/doc_123#comment_456" },
cursor: null,
};
const body = JSON.stringify(event);
const signedAt = new Date();
const signature = new Webhook(secret).sign(event.eventId, signedAt, body);
const headers = {
"webhook-id": event.eventId,
"webhook-timestamp": String(Math.floor(signedAt.getTime() / 1000)),
"webhook-signature": signature,
};
const verified = new Webhook(secret).verify(body, headers);
console.log("signature verifies:", JSON.stringify(verified) === body);
Output on Node 22.22.0:
secret valid: true
short secret valid: false
id stable: true
signature verifies: true
Then run OpenAI's in-product checklist. Your events should appear on your plugin page next to your tools (rescan the MCP server whenever tools or events change). Start a Work chat on web, or select Work and Cloud in the desktop app, or use a dot, and ask ChatGPT to subscribe. Confirm you receive events/subscribe with the expected arguments, verification succeeds, a matching event gets a 2xx, a non-matching event is not delivered, and stopping the monitor sends events/unsubscribe. OpenAI also asks you to test repeated subscribes, expiry and refresh across a restart, account disconnection, revoked access, invalid signatures, duplicate deliveries, bursts with batching on and off, and feedback loops.
Gotchas and limits#
- Webhook-only. ChatGPT does not support the draft's polling or streaming modes, or its
gapandterminatedcontrol notifications. You need durable storage and a reliable outbound sender; you cannot fall back to "let the client poll me." - Error-code drift. OpenAI's doc says
CallbackEndpointErroris-32015. The draft spec's latest commit (Oct 8, 2026, "Renumber error codes to match SEP-3415") moved it to-32027, notes that earlier revisions used-32011to-32015, and calls the new codes provisional until SEP-3415 is accepted. For ChatGPT today, follow OpenAI's doc; keep the code in one constant so you can change it. - Batched and out of order. ChatGPT may group events into one run, and events can arrive out of order. Key on
eventIdand make write tools idempotent. The draft is explicit that exactly-once requires application-level dedup. cursor: nullmeans gaps are permanent. For event types without replay, events missed during an outage cannot be recovered through the protocol. If that matters, implement replayable cursors and returntruncated: truewhen history is gone.- Payload text is untrusted. Comments, messages and titles are user-authored. OpenAI says to treat them as data and never put model instructions inside an event payload. For large records, send a summary and expose a read tool.
- Feedback loops. If the task writes back to the same source (replying to the comment that triggered it), make sure the write does not fire the same event again.
- Run caps. 30 runs per hour and 720 per day across all of a user's event-triggered tasks. Active task limits are 3 for Free and Go, 5 for Plus, 10 for Business and Edu, and 15 for Pro and Enterprise. Scheduled tasks do not support voice chats or GPTs.
- Admin toggle. Enterprise, Edu and ChatGPT for Healthcare admins must enable "Allow event-triggered scheduled tasks" before members can create them. In Healthcare they are off by default and not covered under a BAA, so do not use them for PHI. ChatGPT Health and ChatGPT Finances are not supported event sources.
- Surfaces differ. First-party triggers are created on web and mobile only. MCP-event subscriptions also work in desktop Work chats with Cloud selected and in dots, per the developer doc.
- Inactive tasks may pause, as can tasks that need action or whose chat is archived or deleted. Check Scheduled if something goes quiet.
How it compares: Claude Code Routines#
The closest alternative is Claude Code Routines, which also run agent work from schedules and GitHub events. We cover them in depth in Routines vs Managed Agents schedules. On documented capabilities:
| ChatGPT event-triggered tasks | Claude Code Routines | |
|---|---|---|
| Event sources | First-party Gmail, Slack, GitHub PR activity, plus any plugin that ships MCP events | GitHub pull request and release events, a per-routine API /fire endpoint any system can call, plus schedules |
| GitHub filters | PR, author, title, label, activity type (reviews, comments, commits, merges only) | Author, title, body, base and head branch, labels, draft, merged, with equals, contains and regex operators |
| Bursts | May batch several events into one run | One new session per GitHub event; events past hourly caps are dropped |
| Limits | 30 runs per hour, 720 per day across event tasks | Per-routine and per-account hourly caps on GitHub events; schedules at least one hour apart |
| Mixing triggers | Several event triggers per task, but not events plus a schedule | Schedule, API and GitHub triggers on one routine |
| Where it runs | ChatGPT Work in OpenAI's cloud | Anthropic-managed cloud sessions against your repos |
| Plans | Plus, Pro, Business, Enterprise, Edu, Healthcare (eligible) | Pro, Max, Team, Enterprise; research preview |
Our read: if you control the event source and the outcome is a code change in a repository, Routines' API trigger (a bearer-token POST with the experimental-cc-routine-2026-04-01 beta header) is the lighter integration - no subscription protocol to implement. ChatGPT's MCP events suit products that want end users to wire their own "watch and act" flows from inside ChatGPT, across mail, chat and your app, at the cost of implementing subscribe, verify and sign. Routines' richer GitHub filters are an advantage for engineering workflows; ChatGPT's batching is gentler on bursty sources.
Inside OpenAI's own stack there is one more contrast: desktop scheduled tasks can run in local projects and worktrees, as we covered in Codex Automations, but the desktop app cannot create event triggers.
FAQ#
Can Free users create event-triggered tasks?#
No. Free and Go users are limited to time-based scheduled tasks: three active tasks, run once or recurring no more than once per day. They can still share tasks and open shared links, but running a shared webhook-triggered task needs Work access.
How many times can an event-triggered task run?#
Up to 30 times per hour and 720 times per day, counted across all of a user's event-triggered tasks. ChatGPT may group several matching events into a single run, so a burst does not always mean a burst of runs.
Do I need MCP 2026-07-28 to ship MCP events to ChatGPT?#
Yes. OpenAI requires MCP protocol version 2026-07-28, webhook delivery with Standard Webhooks signatures, persistent subscription storage and outbound HTTPS to callback URLs.
Can one task mix a schedule and an event trigger?#
No. A task can use multiple event triggers, but it cannot combine event triggers with a time-based schedule. Create two tasks if you need both.
Does sharing a task share my Gmail or Slack access?#
No. The link carries a snapshot of the title, instructions, schedule and original time zone. It excludes your credentials, connected app data, chat history and memories, and recipients connect their own apps. Anyone with a personal-account link can read the full instructions, though.
Sources#
- ChatGPT release notes - OpenAI (June 17, July 9, August 25 and September 29, 2026 entries)
- Scheduled tasks in ChatGPT - OpenAI Help Center
- Connected apps in ChatGPT - OpenAI Help Center
- Trigger tasks from app events - ChatGPT docs
- MCP Events - OpenAI Developers
- MCP Events design sketch (draft) - Model Context Protocol
- Standard Webhooks JavaScript library - Standard Webhooks
- Routines - Claude Code docs
Continue Reading#
- OpenAI Apps SDK: Building MCP UIs Inside ChatGPT - the plugin MCP server you would add events to
- Claude Code Routines vs Managed Agents Schedules - Anthropic's trigger model in depth
- OpenAI Dots: Always-On Agents Need a Control Plane - the other surface that can hold MCP event subscriptions
Get the next deep dive like this in your inbox
One email a week on ai-agents and the rest of the AI dev stack. Free.
Read next on AI coding tools
ChatGPT Tasks: Scheduled AI Agents Inside ChatGPT
ChatGPT Tasks lets you schedule reminders and recurring jobs in plain language. See how it works, practical use cases, current limits and where it is heading.
8 min readOpenAI Apps SDK: Building MCP UIs Inside ChatGPT
Apps SDK extends MCP with UI. Here is how to ship a real Apps SDK app from scratch: logic, interface, deploy, distribution, and the gotchas that cost me a weekend.
12 min readClaude Code Routines vs Managed Agents Schedules: Where Recurring Agent Work Should Live
Claude Code Routines vs Managed Agents scheduled deployments: how triggers, pricing and limits differ, and where your recurring agent work should run.
9 min readNew here? Start with
Technical content at the intersection of AI and development. Building with AI agents, Claude Code, and modern dev tools - then showing you exactly how it works.








