Integration prompt
Paste this into Claude Code, Cursor, Windsurf, or any other AI coding agent and it will build you a webhook receiver in your project's stack, set up signature verification correctly, and end with a checklist for the Dovilo-side setup.
I want to receive task events from Dovilo (https://dovilo.app — a gamified
todo app) into this project and act on them. Please build a webhook
receiver here, then walk me through the Dovilo-side setup.
DO THIS IN ORDER:
────────────────────────────────────────────────────────────────────
STEP 1 — ASK ME FIRST
────────────────────────────────────────────────────────────────────
- What should happen when an event arrives? (Slack notification?
Linear sync? CI trigger? Database log? Hand off to an LLM?)
- Which stack should the receiver use? Match this project if obvious;
otherwise ask. (Node/Express, FastAPI, Go net/http, Cloudflare
Workers, Vercel function, etc.)
- Where will it be deployed and what's the public URL going to be?
Public HTTPS URLs are delivered by Dovilo's server. A localhost URL
is delivered by Dovilo Desktop, which must stay open. To test a
public URL from a dev machine, suggest ngrok / cloudflared /
tailscale funnel.
────────────────────────────────────────────────────────────────────
STEP 2 — WRITE THE RECEIVER
────────────────────────────────────────────────────────────────────
Route: POST /webhooks/dovilo (or fit the project's conventions)
CRITICAL RULES:
- Read the RAW request body before any JSON parsing. The HMAC is
computed over the raw bytes; re-serializing breaks the signature.
- Verify `X-Dovilo-Signature` (format: `t=<unix-ms>,v1=<hex>`):
1. Parse `t` and `v1`.
2. Reject if `|Date.now() - t|` > 5 minutes (replay protection).
3. Compute `HMAC_SHA256(secret, `${t}.${rawBody}`)`, hex-encode.
4. Compare to `v1` in CONSTANT TIME (timingSafeEqual /
hmac.compare_digest / hmac.Equal). Never use plain `==`.
- Use `X-Dovilo-Delivery` (also `id` in the body) as the idempotency
key — store it, drop duplicates. Same id is reused across retries.
- Respond 2xx on success and 401 on a bad signature. NEVER answer a
bad signature with 410. Respond 410 ONLY if the integration is
permanently dead: Dovilo disables the project's webhook when it
sees 410.
- Read the signing secret from env var `DOVILO_WEBHOOK_SECRET`. Add
a placeholder to `.env.example`. NEVER commit the real secret.
────────────────────────────────────────────────────────────────────
STEP 3 — HANDLE THESE EVENT TYPES
────────────────────────────────────────────────────────────────────
- `task.created` — new task created.
- `task.status_changed` — body has `previousStatus` + `task.status`.
Status values: scheduled, queued, running,
awaiting-user, done, done-with-errors, error.
A cancelled task ends in error, with
task.cancelledAt set.
- `webhook.ping` — test event from Dovilo's "Send test" button.
Acknowledge with 2xx; don't trigger anything.
Payload shape (slim — these are ALL the fields you get):
{
"id": "evt_...", // also in X-Dovilo-Delivery
"type": "task.status_changed",
"createdAt": 1715900000000, // unix ms when queued
"projectId": "list_...",
"projectName": "Marketing site", // may be omitted
"previousStatus": "running" | null, // status_changed only
"task": {
"id": "tsk_...",
"text": "Draft launch announcement",
"status": "done",
"projectId": "list_...",
"createdAt": 1715800000000,
"updatedAt": 1715900000000, // USE THIS to order, not delivery time
"priority": "high", // optional: none|low|medium|high|urgent
"importance": true, // optional
"scheduledAt": 1715886000000, // optional, unix ms
"isRecurring": true, // optional
"recurrenceType": "weekly", // optional: daily|weekly|monthly|yearly
"seriesId": "tsk_...", // optional, shared by every run
"cancelledAt": 1715900000000, // optional, set when cancelled
"startedAt": 1715890000000, // optional
"completedAt": 1715900000000, // optional
"error": { "message": "...", "stepId": "..." }, // optional, on error
"claimedByAgentId": "agent_...", // optional
"claimedByAgentName": "Claude Code", // optional
"telemetry": { // only on terminal transitions
"entries": 14, "tokensIn": 12483, "tokensOut": 3201,
"durationMs": 84120, "costEstimate": 0.0731, "truncated": false
}
}
}
Ordering: same task = FIFO. Different tasks = parallel, no order
guarantee. Sort by `task.updatedAt` if you need a total order.
────────────────────────────────────────────────────────────────────
STEP 4 — TEST LOCALLY
────────────────────────────────────────────────────────────────────
Show me how to:
- Run the receiver locally.
- Expose it on a public URL if needed (ngrok / cloudflared).
- What logs to watch.
────────────────────────────────────────────────────────────────────
STEP 5 — END WITH THIS CHECKLIST (substitute the real URL)
────────────────────────────────────────────────────────────────────
Print this checklist VERBATIM at the end, replacing `<YOUR_URL>` with
the actual public URL of the receiver you just built:
─────────────────────────────────────────────────────────────
FINAL STEP — set this up on the Dovilo side:
1. Open Dovilo Desktop (2.0 or later) and open the List settings
of the project you want events from.
2. Find the "Webhooks" section.
3. Paste this URL into the URL field:
<YOUR_URL>
4. Tick the events you want (task.created and/or
task.status_changed), switch the webhook to "Enabled" and
press Save.
5. Click "Generate signing secret". The plaintext secret is
shown EXACTLY ONCE: copy it immediately.
6. Put the secret in your `.env` (or your host's secret manager):
DOVILO_WEBHOOK_SECRET=<paste-here>
Then restart the receiver so it picks up the env var.
7. Click "Send test". You should see a 2xx in your receiver logs
within a second.
─────────────────────────────────────────────────────────────
────────────────────────────────────────────────────────────────────
GUARDRAILS
────────────────────────────────────────────────────────────────────
- Don't invent Dovilo features. If I ask for something this prompt
doesn't cover (inbound API, unsubscribe webhook, batch deliveries,
task.deleted events), say "not currently supported by Dovilo
webhooks — check https://dovilo.app/docs/webhooks/" rather than
guessing or stubbing.
- The webhook payload is intentionally slim. If I need the full task
(description, agent notes, full telemetry, steps), tell me to
fetch it via Dovilo's Agents MCP (https://dovilo.app/mcp/agents,
agent key as a Bearer token) using `get_task` / `list_tasks`.
Tip: for the fastest result, run it inside the project that will host the receiver, so the agent picks up your stack, conventions, and lint/test scripts.
Quick start
- In Dovilo Desktop 2.0 or later, open the project's List settings and find the Webhooks section. Webhooks are set up on the desktop only.
- Paste your receiver URL. It must be
https://unless it is a local address; see Delivery modes. - Pick which events you want (
task.created,task.status_changed), switch the webhook to Enabled and press Save. For a public URL the server needs the saved address before it can create a secret. - Click Generate signing secret. The plaintext is shown once: copy it into your receiver's secret store immediately.
- Click Send test to fire a
webhook.pingagainst your endpoint.
Once enabled, every matching task event in that project is delivered as a signed POST to the configured URL.
Delivery modes
Who sends the request depends on the URL. A public HTTPS receiver is delivered by Dovilo's server, so it gets events even when Desktop is closed. A receiver on your own machine or network is delivered by Dovilo Desktop, because Dovilo's server never sends to a private address; the app labels these Local.
| Server delivery | Desktop delivery (Local) | |
|---|---|---|
| Applies to | Public URLs | localhost, *.local, 127.0.0.0/8, 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, 169.254.0.0/16, ::1, fc00::/7, fe80::/10 |
| URL scheme | https:// only; a public http:// URL is not saved | http:// or https:// |
| Desktop must be running | No | Yes. Events while Desktop is closed are not resent |
| Signing secret | Generated on Dovilo's server, stored encrypted, shown once | Generated on this computer, kept in OS secure storage, shown once |
| Send test | From the server; the editor shows the HTTP status your endpoint returned | From Desktop |
After 410 Gone | Delivery stops for that URL until a successful Send test, a new secret or a new URL | The project's webhook is switched to disabled |
| Failure details | Last delivery, last HTTP status and last error in the webhook settings | In-memory failure log in the webhook settings |
Signature, headers, payload, retry schedule and per-task ordering are the same in both modes. The server never delivers to a private address, even when a public hostname resolves to one.
Transport
| Property | Value |
|---|---|
| Method | POST |
| Content-Type | application/json |
| User-Agent | Dovilo-Webhook/1.0 |
| URL schemes | https://; http:// for local addresses only (Delivery modes) |
| Body encoding | UTF-8 JSON |
| Per-attempt timeout | 10 seconds |
Anything outside http(s):// is rejected before a request is made. A public http:// URL is not saved, because the server delivers only over HTTPS. Plain http:// to a local address is allowed, with a warning in the UI; don't ship a production integration over cleartext.
Events
Dovilo currently emits three event types. The set is intentionally small; we'd rather add events later than walk back a half-baked one.
| Event type | When it fires |
|---|---|
task.created | A new task is created in the project. |
task.status_changed | A task's status changes (e.g. queued → running → done). |
webhook.ping | Synthetic event sent by the Send test button. Single attempt, no retry. |
You select which events to subscribe to per project. webhook.ping is always delivered when the test button is pressed, regardless of subscription.
Task lifecycle reference
task.status_changed carries a previousStatus field. The status values you will see in task.status and previousStatus:
scheduled → queued → running → awaiting-user → queued → running → done
→ done-with-errors
→ error
scheduled is a task waiting for its scheduledAt time. Dovilo moves it to queued when the time comes, so previousStatus can be scheduled. A task created without a future time starts at queued.
awaiting-user is entered when an agent pauses for human input (plan approval, mid-task question). Once the user responds, it goes back to queued, and an agent picks it up again on its next turn.
The terminal statuses are done, done-with-errors and error. A cancelled task ends in error with cancelledAt set; an error without it is a failure. For a repeating task, the next run is a new task with the same seriesId, created once the previous one reaches a terminal status.
Headers
Every delivery includes three Dovilo-specific headers in addition to the standard ones.
| Header | Example | Purpose |
|---|---|---|
X-Dovilo-Signature | t=1715900000000,v1=4b2a...c7 | HMAC signature + timestamp (see below) |
X-Dovilo-Event | task.status_changed | Event type: route without parsing body |
X-Dovilo-Delivery | evt_4f0b8a3e-7d2a-4c92-9b13-3e8a1f6c4d11 | Unique delivery id: use for deduplication |
The delivery id is stable across retries of the same delivery, so it is safe to use as an idempotency key in your receiver.
Payload schema
The body is a JSON object. Field set is deliberately slim: just enough to route and decide whether to fetch more. Large or sensitive fields (description, agentNotes, context, steps, full telemetry) are not included; fetch the full task via MCP (get_task) if you need them.
Top-level
{
"id": "evt_4f0b8a3e-7d2a-4c92-9b13-3e8a1f6c4d11",
"type": "task.status_changed",
"createdAt": 1715900000000,
"projectId": "list_2hP9...",
"projectName": "Marketing site",
"previousStatus": "running",
"task": { ... }
}
| Field | Type | Notes |
|---|---|---|
id | string | Same value as X-Dovilo-Delivery. Idempotency key. |
type | string | One of task.created, task.status_changed, webhook.ping. |
createdAt | number (unix ms) | Time the event was queued for delivery, not the task time. |
projectId | string | Dovilo project (custom list) id. |
projectName | string | undefined | Display label resolved at fire time. Omitted if project was deleted. |
previousStatus | string | null | Only present for task.status_changed. null on first transition. |
task | object | Slim task: see below. Absent on webhook.ping. |
task object
{
"id": "tsk_8a1c...",
"text": "Draft launch announcement",
"status": "done",
"projectId": "list_2hP9...",
"createdAt": 1715800000000,
"updatedAt": 1715900000000,
"priority": "high",
"importance": true,
"scheduledAt": 1715886000000,
"isRecurring": true,
"recurrenceType": "weekly",
"seriesId": "tsk_7f02...",
"startedAt": 1715890000000,
"completedAt": 1715900000000,
"error": { "message": "...", "stepId": "step_..." },
"claimedByAgentId": "agent_4d...",
"claimedByAgentName": "Claude Code",
"telemetry": {
"entries": 14,
"tokensIn": 12483,
"tokensOut": 3201,
"durationMs": 84120,
"costEstimate": 0.0731,
"truncated": false
}
}
| Field | Type | Notes |
|---|---|---|
id | string | Task id. |
text | string | Task title. |
status | string | Current status (see lifecycle reference). |
projectId | string | Mirrors top-level projectId. |
createdAt | number (unix ms) | Task creation time. |
updatedAt | number (unix ms) | Use this, not the event arrival time, to order events for the same task. |
priority | string | omitted | none | low | medium | high | urgent. |
importance | boolean | omitted | "Starred" flag. |
scheduledAt | number | omitted | When the task is (or was) scheduled to start, unix ms (UTC). |
isRecurring | boolean | omitted | true while the task repeats; false once repeating was stopped. |
recurrenceType | string | omitted | daily | weekly | monthly | yearly. |
seriesId | string | omitted | Shared by every run of a repeating task (the first run's task id). |
cancelledAt | number | omitted | Set with status error when the task was cancelled, including a single run of a repeating task that the user skipped, unix ms. An error without it is a failure. Occurrences missed while an earlier run was still open are never created, so they send no event. |
startedAt | number | omitted | When the task first entered running. |
completedAt | number | omitted | When the task reached a terminal status. |
error | object | omitted | { message, stepId? }. Present on error, sometimes carried after recovery. |
claimedByAgentId | string | omitted | Id of the agent that picked up the task. |
claimedByAgentName | string | omitted | Display name resolved at fire time. |
telemetry | object | omitted | Only on terminal transitions (done, done-with-errors, error). Aggregated totals. |
Fields with omitted mean: the key is absent from the JSON, not present with a null value. Your receiver should treat missing keys as "not set."
webhook.ping payload
The test delivery uses a stripped-down body:
{
"id": "evt_...",
"type": "webhook.ping",
"createdAt": 1715900000000,
"projectId": "list_2hP9..."
}
No task, no signature differences: same headers, same HMAC scheme.
Signature verification
Every delivery is signed with an HMAC-SHA256 of {timestamp}.{rawBody} using the per-project signing secret you generated in the UI. The format is modelled after Stripe's so it should look familiar.
Algorithm
- Read
X-Dovilo-Signature. It looks liket=1715900000000,v1=4b2a...c7. - Parse out the timestamp (
t, unix ms) and the v1 signature (v1, hex). - Reject if
|now - t|exceeds the 5-minute replay window. - Compute
HMAC_SHA256(secret, "{t}.{rawBody}")and hex-encode. - Compare the computed hex to
v1in constant time.
Critical: Use the raw, unparsed request body bytes. Frameworks that auto-parse JSON often discard the original bytes; re-serializing the parsed object will produce a different signature.
Node.js (Express)
const crypto = require('node:crypto');
const express = require('express');
const app = express();
// Keep the raw body around — JSON.parse re-serialization breaks the HMAC.
app.use(
'/webhooks/dovilo',
express.raw({ type: 'application/json' }),
);
const SECRET = process.env.DOVILO_WEBHOOK_SECRET;
const REPLAY_WINDOW_MS = 5 * 60 * 1000;
function verifyDoviloSignature(req) {
const header = req.get('x-dovilo-signature');
if (!header) return false;
const parts = Object.fromEntries(
header.split(',').map((p) => {
const i = p.indexOf('=');
return [p.slice(0, i).trim(), p.slice(i + 1)];
}),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!Number.isFinite(t) || !v1) return false;
if (Math.abs(Date.now() - t) > REPLAY_WINDOW_MS) return false;
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${t}.${req.body.toString('utf8')}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(v1, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/dovilo', (req, res) => {
if (!verifyDoviloSignature(req)) {
return res.status(401).send('invalid signature');
}
const event = JSON.parse(req.body.toString('utf8'));
// ... handle event ...
res.status(204).end();
});
Python (Flask)
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["DOVILO_WEBHOOK_SECRET"].encode("utf-8")
REPLAY_WINDOW_MS = 5 * 60 * 1000
def verify_dovilo_signature(req) -> bool:
header = req.headers.get("X-Dovilo-Signature", "")
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
try:
t = int(parts["t"])
v1 = parts["v1"]
except (KeyError, ValueError):
return False
if abs(int(time.time() * 1000) - t) > REPLAY_WINDOW_MS:
return False
raw = req.get_data() # bytes — do NOT use request.json
mac = hmac.new(SECRET, f"{t}.".encode("utf-8") + raw, hashlib.sha256)
return hmac.compare_digest(mac.hexdigest(), v1)
@app.post("/webhooks/dovilo")
def dovilo_webhook():
if not verify_dovilo_signature(request):
abort(401)
event = request.get_json(force=True)
# ... handle event ...
return "", 204
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
const replayWindow = 5 * time.Minute
func verify(r *http.Request, secret []byte, body []byte) bool {
header := r.Header.Get("X-Dovilo-Signature")
var t int64
var v1 string
for _, p := range strings.Split(header, ",") {
kv := strings.SplitN(p, "=", 2)
if len(kv) != 2 { continue }
switch kv[0] {
case "t":
n, err := strconv.ParseInt(kv[1], 10, 64)
if err != nil { return false }
t = n
case "v1":
v1 = kv[1]
}
}
if t == 0 || v1 == "" { return false }
now := time.Now().UnixMilli()
if abs(now-t) > replayWindow.Milliseconds() { return false }
mac := hmac.New(sha256.New, secret)
mac.Write([]byte(strconv.FormatInt(t, 10) + "."))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(v1))
}
func handle(w http.ResponseWriter, r *http.Request) {
secret := []byte(os.Getenv("DOVILO_WEBHOOK_SECRET"))
body, _ := io.ReadAll(r.Body)
if !verify(r, secret, body) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
// ... handle event ...
w.WriteHeader(http.StatusNoContent)
}
func abs(x int64) int64 { if x < 0 { return -x }; return x }
Common verification mistakes
- Re-serializing the body. If you
JSON.parse → JSON.stringifybefore hashing, byte-for-byte differences (key order, whitespace) will break the signature. Hash the raw bytes. - String comparison instead of constant time. Use
timingSafeEqual/hmac.compare_digest/hmac.Equal. Plain==leaks timing info. - Skipping the timestamp check. Without it, a captured payload can be replayed forever. Enforce the 5-minute window.
- Trusting the body before verifying. Verify the signature on the raw bytes first, then parse the JSON.
Responses Dovilo expects
Your receiver's HTTP status determines what happens next.
| Status range | Dovilo's behavior |
|---|---|
2xx | Delivered. Done. |
429 Too Many Requests | Treated as transient. Retried per the schedule below. Retry-After is surfaced in failure logs but the next attempt still comes from the standard backoff. |
4xx (other than 429) | Permanent failure. No retry. Recorded in the failures buffer. |
410 Gone | Permanent failure and Dovilo disables the webhook for this project. |
5xx | Treated as transient. Retried per the schedule below. |
| Connection error / DNS / TLS | Treated as transient. Retried. |
| Timeout (>10s) | Treated as transient. Retried. |
Respond quickly: under a few hundred ms is ideal. If your processing is slow, accept the delivery (202) and queue the work asynchronously rather than holding the request open.
Use 410 Gone deliberately. It is the documented "this endpoint is dead, stop sending" signal. What happens next depends on the delivery mode: Desktop switches the project's webhook to disabled, and the server stops delivering to that URL.
Answer an invalid signature with 401, never 410. A request that fails verification may come from an older Dovilo Desktop still signing with a secret you have since replaced. 410 Gone disables the subscription, so a 410 sent to that request would switch off the webhook for the whole project, server delivery included.
Retry policy
Failed transient deliveries are retried up to 5 times on this schedule:
| Attempt | Wait before attempt |
|---|---|
| 1 | 0 |
| 2 | 5 seconds |
| 3 | 30 seconds |
| 4 | 2 minutes |
| 5 | 10 minutes |
| 6 | 1 hour |
Total delivery window is therefore approximately 1 hour 15 minutes. After the final attempt, the delivery is dropped.
- Server delivery: the webhook settings show the last delivery, its HTTP status and the last error.
- Desktop delivery: the failure is recorded in the failures ring buffer in the webhook settings (last 100 entries per project). The buffer is in-memory on the desktop client and is not persisted across app restarts, so treat it as a debugging aid, not an audit log. A pending retry is lost if Desktop quits.
Ordering and concurrency
- Same task: events are delivered in FIFO order. A retry of an older event will not pass a newer event for the same task.
- Different tasks: events run in parallel. There is no ordering guarantee across tasks.
- Receivers should not rely on delivery time. Use
task.updatedAt(andpreviousStatusfor transitions) to reconstruct order.
Idempotency
Use X-Dovilo-Delivery (also available as id in the body) as your idempotency key. The same delivery id is reused across retries of the same event, so deduplication on this key is both safe and correct.
Recommended pattern: insert the delivery id into a unique-keyed table inside the same transaction as your side effects. On retry, the duplicate key collision becomes a no-op.
Security
- The signing secret is the only thing standing between your endpoint and a spoofed Dovilo request. Treat it like a password.
- Anyone who learns the secret can forge requests that pass verification.
- Rotate the secret if you suspect compromise: generating a new one in the UI immediately invalidates the previous secret.
- Plaintext is revealed exactly once, when generated. There is no way to retrieve it later: generate, copy into your secret store, move on.
- Server delivery: the secret is generated and stored by Dovilo's server, encrypted. It is never written where the apps can read it back.
- Desktop delivery: the secret is stored by the desktop client using OS-level secure storage (Keychain / Credential Manager / libsecret). It is not written to Firestore and is not synced across devices.
Operational notes
- Desktop-bound delivery (Local only). Local webhooks fire from the Dovilo desktop app process, so they will not deliver when the app is closed. Events that occur while the app is closed are not queued for later delivery; they are simply not sent. Tasks already present in Firestore at app launch are treated as historical state, not new events, and do not fire.
- Mobile- and agent-originated changes fire too. Server delivery observes every change. For Desktop delivery, while the desktop is running, any task created or transitioned by mobile or an MCP agent is observed via Firestore sync and fires the same events as a local write. The desktop persists per-task dedup markers (
webhookCreatedFiredAt,webhookLastSentStatus) inside the same Firestore write, so a snapshot echo or a second connected desktop observing the same change will not double-fire. - Multi-desktop dedup (Desktop delivery). When two desktops are signed in to the same account they race for each cloud-driven event via a Firestore transactional compare-and-swap on the dedup markers. Only the winner delivers, so receivers do not need to dedup across desktops on top of
X-Dovilo-Delivery. - Slim payloads on purpose. Bulky and sensitive fields are excluded to reduce both bandwidth and the chance of leaking prompt content into third-party integrations. Fetch the full task with
get_taskorlist_tasksover Agents MCP (https://dovilo.app/mcp/agents, agent key asAuthorization: Bearer), which works while Desktop is closed. - No receiver? Poll
list_tasksover Agents MCP withupdated_afterset to the largestupdatedAtfrom the previous response. - No webhook events for deleted tasks. A
task.deletedevent is not currently emitted. If you need to know about deletions, reconcile periodically through MCP.
Changelog
- v0.6.0 — Initial outbound webhook support. Events:
task.created,task.status_changed,webhook.ping. HMAC-SHA256 signing. 5-attempt retry. Per-task FIFO. - v0.6.x — Mobile- and agent-driven changes now fire via Firestore sync diff (previously only desktop-initiated writes fired). Persisted per-task dedup markers prevent snapshot-echo refire and double-fire across multiple connected desktops.
- Dovilo 2.0: Server delivery for public
https://URLs: signing secret, test and delivery status from the server, and delivery while Desktop is closed. Local URLs stay with Desktop. Publichttp://URLs are not accepted. - Dovilo 2.0: Scheduled and repeating tasks. New status value
scheduled, sopreviousStatuscan bescheduled. Task payload:scheduledAt,isRecurring,recurrenceTypeandseriesIdadded; receivers that ignore unknown keys need no change for these.dueAtremoved: it was not in use, but a receiver that read it should stop. - Dovilo 2.0:
cancelledAt(unix ms) on cancelled tasks, including a skipped run of a repeating task, so a cancel can be told apart from a failure without parsingerror.message. Additive. - Correction: the terminal status values are
done,done-with-errorsanderror. Earlier versions of this page listedfailedandcancelled; a receiver that comparestask.statusagainst those strings should check forerror, and forcancelledAt, instead.