Phone alerts for blocked and finished tasks with ntfy
By Jake Marsh, who builds Taskhold. Updated 2026-10-26. Facts about other tools checked 2026-10-26. 3 min read.
The live board sends desktop notifications while it is open. For your phone with the board closed, this script follows Taskhold's event stream and publishes to an ntfy topic: high priority when an agent needs you, normal when it finishes. Tap the notification to open the board and reply.
Prefer Slack? See Slack alerts. Answering from the phone is covered in answering a blocked coding agent.
What you get
One ntfy message when an agent blocks a task with a question, and one when it finishes a whole task (not each step). Each says which agent, the task title, the question or the summary, and links to the live board, where you reply. Your own changes never trigger a message.
Before you begin
- The ntfy app on your phone, subscribed to a topic name nobody would guess (topics on ntfy.sh are public to anyone who knows the name).
- A Taskhold API key from Settings > API keys, named after this script so you can revoke it on its own.
- bash, curl and jq on the machine that runs the script.
The script
Save this as taskhold-ntfy.sh and make it executable (chmod +x). The top half is the ntfy part; the loop below it is the same for every notifier.
#!/bin/bash
# taskhold-ntfy.sh: push to ntfy when an agent blocks or finishes a Taskhold task.
# Needs curl and jq. Run: TASKHOLD_API_KEY=atd_sk_... ./taskhold-ntfy.sh
key=${TASKHOLD_API_KEY:?set TASKHOLD_API_KEY}
board=https://taskhold.com/dashboard/live
last=""
topic=${NTFY_TOPIC:?set NTFY_TOPIC}
notify() { # kind, title, detail, agent
if [ "$1" = blocked ]; then t="$4 needs you: $2"; p=high; tag=warning
else t="$4 finished: $2"; p=default; tag=white_check_mark; fi
curl -s -o /dev/null -H "Title: $t" -H "Priority: $p" -H "Tags: $tag" -H "Click: $board" \
--data-raw "${3:-No details}" "https://ntfy.sh/$topic"
}
while :; do
while IFS= read -r line; do
case "$line" in
"id: "*) last=${line#id: } ;;
"data: "*)
e=${line#data: }
# Agents only: a task (not a step) finished, or any task or step blocked.
jq -e '.type == "task.status" and .actor == "agent" and (.data.to == "blocked" or (.data.to == "done" and .task_id == .root_task_id))' <<<"$e" >/dev/null || continue
kind=$(jq -r .data.to <<<"$e")
title=$(jq -r '.task_title // "a task"' <<<"$e")
agent=$(jq -r '.actor_name // "An agent"' <<<"$e")
detail=$(jq -r 'if .data.to == "blocked" then .data.blocked_reason else .data.completed_summary end // ""' <<<"$e")
notify "$kind" "$title" "$detail" "$agent" ;;
esac
done < <(curl -sN https://taskhold.com/api/events/stream -H "Authorization: Bearer $key" ${last:+-H "Last-Event-ID: $last"})
sleep 3
doneRun it anywhere a process can stay up, such as a spare terminal, a tmux window or a small server:
NTFY_TOPIC=taskhold-7f3k2q TASKHOLD_API_KEY=atd_sk_... ./taskhold-ntfy.shThe server ends each stream after 10 minutes and the script reconnects with the last event id, so events that happen in between arrive once, in order. Details on the stream: events and the live stream.
One limit to know
ntfy.sh caps anonymous publishing (250 messages a day at the time of writing), which a busy day of agents won't reach, and anyone who learns the topic name can read your task titles. Use a long random topic, or your own ntfy server with an access token (add -H "Authorization: Bearer tk_..." to the curl line).
See how far along your coding agents are.
$12/month or $120/year after the trial. Card up front. Cancel before day 7 and you pay nothing. Every agent you run is included.
Sources
- ntfy docs: publishing, checked 2026-10-26
Related
Updated 2026-10-26. Something wrong or out of date? Email hello@greatwork.company.