AI & Agents

Get a Push Notification When an OpenAI Agents SDK Run Finishes

Add a push notification to an OpenAI Agents SDK run in about ten lines. Walk away from the terminal and let your phone tell you when the agent is done.

Red and black halftone illustration: the end of a conveyor line where a finished part is dropping into an otherwise empty bin (TheNotificationApp)

You kick off an agent to triage a backlog of GitHub issues. It has a dozen tools, a hundred issues to read, and no idea how long it will take. You start it, watch three tool calls scroll past, and tab away. Forty minutes later you remember it exists. It finished in nine.

That thirty-one minutes is the whole problem. Not the agent's speed, your attention. The SDK already knows the exact moment the run ended; it just has no way to tell you, because you are not looking at the terminal.

Fixing it takes one HTTP call in the right place. Here is where that place is.

What you need

  • The openai-agents package and an agent you already run
  • An app key from TheNotificationApp, which takes about a minute to get
  • The iOS app installed and signed in with the same Apple ID

No SDK for the notification side, no webhook server, no polling loop. It is one POST to one endpoint, which is why this drops into an agent script without dragging in a dependency you then have to keep alive.

Step 1: Grab your app key

Open the app, sign in with Apple, and create an application. Call it something you will recognise in a notification, like Agents. Tap into it and copy the app_key. That key is the only credential in this post. It maps to your phone, so anything you send with it lands on your lock screen and nobody else's.

Put it in an environment variable rather than in the file. Agent scripts get shared, pasted into issues, and committed more often than most code.

export TNA_APP_KEY="your_app_key_here"

Step 2: Write the notify function

The whole integration is this. Two required fields, two optional ones:

import os
import requests

TNA_URL = "https://thenotification.app/api/sendNotification"

def notify(title: str, body: str, link: str | None = None) -> None:
    """Push a notification. Never raises: a failed alert must not kill the run."""
    try:
        requests.post(
            TNA_URL,
            headers={
                "app_key": os.environ["TNA_APP_KEY"],
                "Content-Type": "application/json",
            },
            json={"title": title, "body": body, **({"link": link} if link else {})},
            timeout=10,
        )
    except Exception as exc:
        print(f"[notify] failed: {exc}")

The try/except matters more than it looks. This function runs at the end of a job that may have taken forty minutes. If the notification call raises inside a finally block, it can mask the agent's own exception and you lose the traceback you actually needed. Swallow it and print instead.

The full field list, including image, is in the API reference.

Step 3: Wrap the run

The version that works with every SDK release, because it does not touch SDK internals at all:

import asyncio, time
from agents import Agent, Runner

agent = Agent(
    name="Issue triage",
    instructions="Read each open issue, label it, and flag anything urgent.",
)

async def main():
    started = time.monotonic()
    try:
        result = await Runner.run(agent, "Triage the open issues in this repo.")
    except Exception as exc:
        notify("Agent failed", f"{type(exc).__name__}: {exc}"[:180])
        raise
    else:
        mins, secs = divmod(int(time.monotonic() - started), 60)
        notify(f"Agent finished in {mins}m {secs}s", str(result.final_output)[:180])
        return result

asyncio.run(main())

Note the raise after the failure notification. You want the alert and the traceback, not the alert instead of the traceback. And note that the failure path sends the exception type and message, because "the agent failed" is a notification that tells you to go look, while "RateLimitError: 429" is one that tells you what happened.

Step 4: Put the real result in the body

The temptation is to send "Done." Resist it. The point of the notification is that you can decide from the lock screen whether to walk back to the desk, and "Done" forces you to walk back to find out.

The agent's own final_output is usually the right body text. Trim it, because iOS shows roughly the first 180 characters before truncating:

summary = str(result.final_output).strip().replace("\n", " ")
notify(
    f"Triage done in {mins}m {secs}s",
    summary[:180],
    link="https://github.com/you/repo/issues",
)

The link field turns the notification into a tap-through. Point it at whatever you would open next, and the alert stops being a heads-up and becomes the first step of the next thing you do.

Step 5: Move it into RunHooks once you have more than one agent

The wrapper is fine for one script. Once you have several agents, the SDK's lifecycle hooks put the notification in one place instead of at the bottom of every file:

from agents import Agent, RunHooks, Runner

class NotifyHooks(RunHooks):
    async def on_agent_end(self, context, agent, output):
        notify(f"{agent.name} finished", str(output)[:180])

result = await Runner.run(agent, "Triage the open issues.", hooks=NotifyHooks())

One caution: on_agent_end fires per agent, so in a handoff chain with four agents you get four notifications. That is usually not what you want. Either filter on the agent name, or keep the outer wrapper for the "everything is done" ping and use hooks only for the specific step you care about.

When to notify, and when not to

A notification on every tool call is not observability, it is a phone you stop looking at. Three triggers are worth it:

  • The run finished. The headline case. Anything over a couple of minutes qualifies.
  • The run died. Especially unattended ones. A crashed agent that nobody notices is just a slow agent.
  • The agent needs a decision. If your agent pauses for approval, that pause is dead time until you know about it. There is a whole pattern for this in human-in-the-loop notifications.

The honest part

The free tier is 100 notifications total, across the lifetime of the account, not per month. For "tell me when the nightly triage run is done" that lasts a long time. For an agent that fires on every tool call it lasts an afternoon. Pro is $2.99 a month for 1,000 notifications, which is around 33 runs a day before you would notice.

The real constraint is not the quota, it is that this is iPhone only. The push rides Apple's own service, so if your phone is an Android you want a different tool and this post is not for you.

Worth it?

If your agents finish in ten minutes and you find out in forty, the gap is not a tooling problem, it is a notification problem, and it costs about ten lines to close. Grab a key at thenotification.app and wrap the next long run you would rather not babysit.

New to this? Start with what an MCP server is.