Let ShotGrid Tell You What Changed: Event-Driven Automation with EventLogEntry
ShotGrid already records every change as an EventLogEntry. Here's how the shotgunEvents daemon turns that stream into auto-created tasks, why handlers must be idempotent, and when event-driven beats polling.
29 Apr 2026, 19:20 UTC

Someone creates twelve new Shots in ShotGrid (now branded Autodesk Flow Production Tracking), and each one needs the same three tasks — Anim, Light, Comp — with the right assignees and dates. A coordinator can click through that setup by hand, or a script can do it the moment each Shot appears. The second option is what ShotGrid's event stream is for, and it's a better foundation than the cron-job polling most studios start with.
Every change is already being recorded
ShotGrid writes an EventLogEntry record for UI- and API-driven changes: entity creation, attribute updates, retirements, deletions. Each entry identifies the affected entity, the acting user, and a meta payload describing what actually changed (for example, which attribute moved to which value). That means "what happened since I last looked" is a question the system can already answer — you don't have to diff the whole database yourself.
The open-source shotgunEvents daemon is the community-standard way to consume this stream. It polls for new EventLogEntries in event-ID order, dispatches each one to registered Python plugin callbacks, and checkpoints its position so it can resume after a restart — or deliberately skip ahead after a long outage. One caveat before you build on it: it's a community project, not an Autodesk-supported component. Check the repository's maintenance status and Python compatibility, and confirm current field names and event-type strings against the Flow Production Tracking docs, since those details shift between versions.
A worked example: default tasks on Shot creation
The canonical plugin listens for Shot creation events and creates a standard task set. A handler sketch looks like this:
import shotgun_api3
TASK_TEMPLATE = ["Anim", "Light", "Comp"]
def registerCallbacks(reg):
reg.registerCallback(
"MyScriptName", # script name matching daemon config
"my_api_key", # script key from ShotGrid admin page
on_shot_created,
{"Shotgun_Shot_New": ["*"]}, # event filter: any new Shot
None,
)
def on_shot_created(sg, logger, event, args):
shot = event["entity"]
existing = sg.find(
"Task",
[["entity", "is", {"type": "Shot", "id": shot["id"]}]],
["content"],
)
existing_names = {t["content"] for t in existing}
batch = [
{
"request_type": "create",
"entity_type": "Task",
"data": {
"content": name,
"entity": {"type": "Shot", "id": shot["id"]},
"project": event["project"],
},
}
for name in TASK_TEMPLATE if name not in existing_names
]
if batch:
sg.batch(batch)
Three things in this sketch are deliberate, not decorative:
- The existence check makes it idempotent. If the daemon dies after processing an event but before saving its checkpoint, that event will be delivered again on restart. Without the check, you'd get duplicate Anim tasks. With it, reprocessing is harmless.
- One
batch()call instead of threecreate()calls. When a single event fans out into many writes, batching cuts HTTP round trips. Check the current docs for exact batch error semantics before relying on partial-failure behavior. - The handler does no heavy work. It validates and writes. If you needed to render thumbnails or sync to another system, you'd enqueue that onto a job queue here and return immediately.
Why ordering shapes your design
The daemon processes events in event-ID order, one at a time through each plugin. That's a feature — you see changes in the sequence they happened — but it means a slow handler delays every event behind it. A handler that blocks for 30 seconds on an external API call turns the event stream into a latency queue. The standard fix is the one above: handlers stay thin, heavy work goes to a background queue, and the daemon keeps up with the stream.
Ordering also matters for your outage policy. The checkpoint means the daemon can catch up on everything it missed — but if it was down for a week, "catch up" might mean replaying thousands of stale events. Decide in advance whether your handlers tolerate that, or whether you skip ahead to the latest event ID and accept the gap.
Event stream vs. polling: the real trade-off
The alternative is a cron job that queries for recently updated entities with the regular query API. It's simpler to deploy — no daemon to babysit — and for low-stakes syncs it's fine. But it has real costs:
- Latency. Your sync is only as fresh as your poll interval, and tightening the interval raises API load on every poll, even when nothing changed.
- Blind spots. Retirements and deletions are easy to miss when you're querying for things that exist; the event log records them explicitly.
- Operational burden flips. Polling has no daemon to monitor, but the event daemon needs real care: it runs on your infrastructure with dedicated script credentials, needs logging and alerting, and needs the outage policy above.
The honest summary: polling is cheaper to start, events are cheaper to run correctly at scale. If the automation drives production decisions — task creation, status propagation, downstream publishes — the event stream's completeness and ordering are worth the ops investment.
Try it before you trust it
On a trial site, make an edit in the UI, then query EventLogEntry through the API and inspect the event_type and meta payloads firsthand — that's the fastest way to learn what your handlers will actually see. Then run the daemon against the test site, kill it mid-stream, and restart it. Watch whether events are redelivered and whether your idempotency checks hold. That fifteen-minute exercise will teach you more about your failure modes than any amount of doc reading — and it's the difference between automation you trust and automation that quietly creates duplicate tasks at 2 a.m.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.