Hi all,
Since around August 2, 2026, the polling Events API (GET /events?resource=<project_gid>) has stopped delivering task changed events for most field changes in our workspace. Our integration syncs custom field values off change.field == "custom_fields" events, exactly as described in the Events reference, and those events simply no longer arrive.
What we tested
Fully scripted, reproduced several times on 2026-08-04. Workspace 1212889913907655 (developer sandbox domain), plain PAT.
- Create a plain project via the API (no template, no portfolio), create a text custom field via
POST /custom_fields, attach it withaddCustomFieldSetting, create a task. - Establish an events sync token for the project and drain the setup events (those arrive fine).
PUT /tasks/{task}changing onlyname→ achangedevent withchange.field: "name"arrives within ~10 seconds.
PUT /tasks/{task}changing only the custom field value → the write succeeds (200, value confirmed by reading the task back), but no event of any kind arrives within 120+ seconds — no taskchangedevent, not even a story event.
Results across field types (each change made in isolation, same event stream):
| Field changed | Task changed event |
Story event |
|---|---|---|
| name | — | |
| custom field (text and enum tested) | ||
| notes | ||
| due_on | story added only |
|
| completed | story added only |
|
| section membership |
For due_on and completed, the only signal left is the added event for the activity-feed story, which carries change: null and no field information. For custom fields and notes there is no signal at all.
Things we ruled out
- Not the token: a freshly created PAT for the same user reproduces it identically.
- Not the project or field setup: happens both on template-instantiated projects and on plain API-created projects with API-created fields.
- Not rate limiting: no 429s anywhere, all requests return 200, and the missing events would have come through the same successful polling responses that deliver the name/section events.
- The integration received these events correctly until at least August 2, 2026, with no changes on our side.
I know the events system is documented as at-most-once delivery, but this is deterministic 100% suppression of entire event classes, not occasional loss.
Was there an intentional change to which field changes emit events? If so, where is it documented, and what is the recommended way to detect custom field value changes on tasks going forward? If not, this looks like a regression.
Repro script
"""Repro: which task field changes still emit Events API events?
Creates a scratch project + text custom field + task, then changes
name / custom field / due_on / notes / completed one at a time,
polling the project's event stream between each. Cleans up at the end.
"""
import json
import os
import time
import urllib.error
import urllib.parse
import urllib.request
PAT = os.environ["ASANA_PAT"]
WORKSPACE = os.environ["ASANA_WORKSPACE_GID"]
TEAM = os.environ["ASANA_TEAM_GID"]
BASE = "https://app.asana.com/api/1.0"
def call(method, path, data=None, params=None):
"""Make an Asana API request and return the parsed JSON body (errors included)."""
url = f"{BASE}{path}"
if params:
url += "?" + urllib.parse.urlencode(params)
body = json.dumps({"data": data}).encode() if data is not None else None
req = urllib.request.Request(url, data=body, method=method)
req.add_header("Authorization", f"Bearer {PAT}")
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req) as resp:
return json.load(resp)
except urllib.error.HTTPError as e:
return json.load(e)
def poll_until(project_gid, token, label, timeout=120):
"""Poll events every 10s until any arrive or timeout; print them. Returns newest token."""
deadline = time.time() + timeout
while time.time() < deadline:
time.sleep(10)
resp = call("GET", "/events", params={"resource": project_gid, "sync": token})
token = resp.get("sync", token)
events = resp.get("data", [])
if events:
print(f" [{label}] {len(events)} event(s):")
for ev in events:
print(" ", json.dumps({
"action": ev.get("action"),
"type": (ev.get("resource") or {}).get("resource_type"),
"change": ev.get("change"),
}))
return token
print(f" [{label}] NO events within {timeout}s")
return token
project_gid = call("POST", "/projects", {"name": "Event Repro", "team": TEAM})["data"]["gid"]
field_gid = call("POST", "/custom_fields", {
"name": "Repro Text Field", "resource_subtype": "text", "workspace": WORKSPACE,
})["data"]["gid"]
call("POST", f"/projects/{project_gid}/addCustomFieldSetting", {"custom_field": field_gid})
task_gid = call("POST", "/tasks", {"name": "Repro Task", "projects": [project_gid]})["data"]["gid"]
print("project:", project_gid, "field:", field_gid, "task:", task_gid)
token = call("GET", "/events", params={"resource": project_gid}).get("sync")
token = poll_until(project_gid, token, "drain-setup", timeout=30)
MUTATIONS = [
("name", {"name": "Repro Task RENAMED"}),
("custom_field", {"custom_fields": {field_gid: "hello-events"}}),
("due_on", {"due_on": "2026-08-15"}),
("notes", {"notes": "repro notes body"}),
("completed", {"completed": True}),
]
for label, payload in MUTATIONS:
print(f"\n== change {label} ==")
result = call("PUT", f"/tasks/{task_gid}", payload)
print(" PUT:", "ok" if "errors" not in result else result["errors"])
token = poll_until(project_gid, token, label)
print("\ncleanup:")
print(" delete project:", "ok" if "errors" not in call("DELETE", f"/projects/{project_gid}") else "FAILED")
print(" delete field:", "ok" if "errors" not in call("DELETE", f"/custom_fields/{field_gid}") else "FAILED")
Thanks!
Thanks!