Skip to content
LastPing
GUIDE · TEMPORAL · FREE

Monitor Temporal workflows: activity heartbeat and LastPing completion ping

In the @workflow.run method, wrap execute_activity in try/except: on success execute a dedicated ping activity that GETs the LastPing URL; on exception execute a fail-ping activity that POSTs to /fail. Use activity.heartbeat() to prevent Temporal from timing out long activities. Absence detection covers a down worker. Free, no monitor cap.

Your assistant can do this for you

Monitor my Temporal workflows with LastPing

On this page
  1. The snippets
  2. What this catches
  3. Questions people ask

The snippets

Create a monitor in the console, set the schedule to match your Temporal cron_schedule plus a grace window, and add the ping activities below.

Python SDK: ping as a dedicated activity

Temporal replays workflow code deterministically, so HTTP calls must go inside activities, not directly in the workflow body. A small ping_lastping activity is the correct pattern:

import httpx
from datetime import timedelta
from temporalio import activity, workflow
from temporalio.client import Client
from temporalio.worker import Worker

PING = "https://ping.lastping.dev/<your-monitor-id>"

@activity.defn
async def ping_lastping(suffix: str = "", body: str = "") -> None:
    async with httpx.AsyncClient() as client:
        if body:
            await client.post(
                f"{PING}{suffix}",
                content=body[:10_000].encode(),
                timeout=10,
            )
        else:
            await client.get(f"{PING}{suffix}", timeout=10)

@activity.defn
async def run_etl() -> None:
    # your ETL work
    for i, chunk in enumerate(get_chunks()):
        process(chunk)
        activity.heartbeat(f"chunk {i} done")  # keeps Temporal's timeout alive

@workflow.defn
class DailyEtlWorkflow:
    @workflow.run
    async def run(self) -> None:
        try:
            await workflow.execute_activity(
                run_etl,
                schedule_to_close_timeout=timedelta(hours=2),
            )
            await workflow.execute_activity(
                ping_lastping,
                schedule_to_close_timeout=timedelta(seconds=30),
            )
        except Exception as e:
            await workflow.execute_activity(
                ping_lastping,
                args=["/fail", str(e)[:10_000]],
                schedule_to_close_timeout=timedelta(seconds=30),
            )
            raise

Registering the workflow with a cron schedule

Pass cron_schedule to start_workflow so Temporal re-runs the workflow on the given cron expression. Each run is independent: the workflow function starts fresh each time:

async def main():
    client = await Client.connect("localhost:7233")

    await client.start_workflow(
        DailyEtlWorkflow.run,
        id="daily-etl-workflow",
        task_queue="etl-queue",
        cron_schedule="0 2 * * *",  # 02:00 UTC every day
    )

    async with Worker(
        client,
        task_queue="etl-queue",
        workflows=[DailyEtlWorkflow],
        activities=[run_etl, ping_lastping],
    ):
        await asyncio.sleep(float("inf"))  # keep worker running

if __name__ == "__main__":
    asyncio.run(main())

activity.heartbeat() vs. LastPing ping: two different things

activity.heartbeat() is an internal keepalive that prevents Temporal from declaring the activity timed out. A LastPing ping is an external signal to your team that the workflow completed on schedule. Both are needed for long-running workflows: heartbeats keep the activity alive; LastPing tells you when it's done. See also the Airflow guide for a DAG-level comparison and the LastPing MCP server for AI-agent provisioning.

Grace window alignment

Set the LastPing grace window to at least schedule_to_close_timeout + retry_budget so Temporal's built-in retries don't open a spurious incident. Only the final failure (after Temporal exhausts retries) should trigger the /fail ping from the workflow's except block.

What this catches

The activities report what they can; the silence reports the rest.

  • Workflow exception after retries: the except block executes the fail-ping activity, opening an incident immediately with the error text.
  • Successful workflow completion: the ping activity fires after the last business activity, resetting the absence timer.
  • Worker is down: no activity is polled; the workflow stays scheduled but never runs; the missing ping opens the incident after the grace window.
  • Activity timeout: Temporal marks the activity as timed out and the workflow's except block fires the fail-ping activity.
  • Recovery: the next successful workflow run fires the success ping activity, closing the incident automatically.

Questions people ask

Front-loaded answers: the most important fact first.

  • How do I monitor a Temporal workflow with LastPing?

    In the workflow's @workflow.run method, wrap execute_activity in try/except: on success execute a dedicated ping activity that makes an httpx GET to https://ping.lastping.dev/<your-monitor-id>; on exception execute a fail-ping activity that POSTs the error text to /fail, and re-raise.

  • Where should I place the LastPing ping: in the workflow or in an activity?

    Place it in the workflow's run method, after the final activity completes, but not as a direct HTTP call. Temporal replays workflow code deterministically, so side effects, including fetch or httpx calls, must go inside activities: wrap the ping in a dedicated @activity.defn function and call it with workflow.execute_activity.

  • What is the difference between activity.heartbeat and a LastPing ping?

    activity.heartbeat() is an internal keepalive to Temporal's server: it tells Temporal the activity is still running and prevents it from timing out. A LastPing ping is an external completion signal that tells your team the workflow completed on schedule. They serve different purposes and both are useful for long-running workflows: one prevents Temporal timeouts, the other prevents silence going unnoticed.

  • What if the Temporal worker process is killed mid-activity?

    Temporal schedules the activity for retry on another worker. If no worker is available (all workers are down), the activity stays in the task queue and no success ping fires. Absence detection opens the incident after the grace window.

  • What if the Temporal worker is down?

    If no worker is polling the task queue, Temporal schedules the workflow but it never starts. No ping fires. Absence-detection monitoring catches this: the missing ping after the schedule+grace window opens an incident.

  • Is Temporal workflow monitoring free?

    Yes. LastPing is free for individuals with no monitor cap. See how it compares to Healthchecks.io and Cronitor.

FREE FOR INDIVIDUALS · FULLY HOSTED

Temporal retries silently. LastPing tells you when they're exhausted.

One ping activity now beats debugging a failed nightly workflow later. First alert wired up in about a minute, and LastPing monitors itself the same way.