Scheduled and Event-driven
Automations start from one of two entry points: a schedule fires at a predetermined time, or an event arrives and triggers a run. Both converge on whichever execution path the trigger’s target dictates — a single-turn agent invocation lands in an AUTO- thread, a multi-step routine lands as a routine_executions row with step-event logging. The difference is all at the front; the downstream model is uniform within its shape. Routing both entry points through the same execution layer is what gives the Traceability operating guarantee its consistency at the automation tier — a scheduled monthly digest and an event-triggered triage flow look identical in the audit log, identical in the cost ledger, identical in the operator’s queue.
The two entry points
Section titled “The two entry points”Scheduled
Section titled “Scheduled”A schedule fires on a cron- or rate-like cadence: “every weekday at 9am,” “every 30 minutes,” “at 00:00 on the first of the month.” Scheduling in ThinkWork is backed by AWS Scheduler (the newer service that replaced most EventBridge Scheduled Rules), because Scheduler gives you one-shot schedules, timezone support, dead-letter handling, and a cleaner primitives model than EventBridge rules.
Every schedule in ThinkWork has:
- A cron or rate expression.
- A target — which agent to invoke, with which prompt, against which thread scope.
- A start and optional end window.
- A tenant + Space scope — schedules are tenant-scoped and usually run in a specific Space.
Schedules are created and managed in the admin app (Scheduled Jobs surface) or via the GraphQL createScheduledJob mutation.
Event-driven
Section titled “Event-driven”An event-driven automation fires when something external happens: a webhook arrives, a integration event lands, an external task changes state, a KPI crosses a threshold. Event-driven automations in ThinkWork route through EventBridge (the custom event bus, not scheduled rules). A triggering event carries a detail-type, source, and detail payload; ThinkWork’s event trigger rules match on those fields.
Event-driven triggers in ThinkWork can be configured for:
- Integration events (
GitHub: new issue,Slack: app mention in #ops). - External task events (
Task assigned to me). - Internal events (agent completed a turn of a specific type; a guardrail fired; a budget was hit).
- Webhook events (custom external webhook routed into the event bus).
The provisioning chain
Section titled “The provisioning chain”The non-obvious part is what happens between “user created a schedule in the admin app” and “a schedule actually fires.” There’s a provisioning chain:
User creates schedule (admin app) │ GraphQL: createScheduledJob ▼scheduled_jobs row inserted in Aurora │ ▼job-schedule-manager Lambda invoked │ (reads scheduled_jobs, calls AWS Scheduler API) ▼AWS Scheduler: CreateSchedule (or UpdateSchedule) │ ▼When the schedule fires:AWS Scheduler → job-trigger Lambda │ (resolves the job row; starts a routine SFN execution OR │ wakes the agent for single-turn work) ▼For routine targets: triggerRoutineRun → SFN.StartExecution → routine_executionsFor agent targets: AgentCore invocation → AUTO- thread → thread_turnsThe chain exists because schedules are first-class ThinkWork records (scheduled_jobs in Aurora), not bare AWS Scheduler entries. The manager Lambda is what keeps the two in sync — if you update a schedule in the admin app, the manager reconciles the change to AWS Scheduler; if you delete a schedule, it removes the Scheduler entry.
This indirection matters:
- You can query, audit, and enforce policy on the Aurora records. Direct AWS Scheduler entries would be opaque to tenant-scoped GraphQL.
- Cross-tenant bulk operations are possible. Operators can disable every schedule for a tenant by updating one column; the manager picks up the change.
- Schedule history is durable. Aurora keeps the row even after a schedule is deleted, so audit queries like “what did we have scheduled last March?” are answerable.
A scheduling pitfall worth knowing
Section titled “A scheduling pitfall worth knowing”If you’re authoring schedules, know this: rate() expressions in AWS Scheduler are not wall-clock. A rate(1 day) schedule fires 24 hours after its creation timestamp, not at midnight. If you want “every day at 00:00 UTC,” use a cron() expression.
This bites people who create a schedule expecting a tidy daily run and discover their “daily digest” is firing at 3:47pm because that’s when the schedule was provisioned. The manager Lambda does not normalize this — if you ask for rate(1 day), you get rate(1 day) exactly as AWS Scheduler implements it.
The admin app Scheduled Jobs form steers authors toward cron() expressions for this reason.
Debugging a schedule that didn’t fire
Section titled “Debugging a schedule that didn’t fire”When a scheduled automation doesn’t run, work the chain:
- Did AWS Scheduler attempt to fire? Check the Scheduler’s history in the AWS console. A dead-letter entry tells you the target invocation failed.
- Did
job-triggerreceive the invocation? CloudWatch logs for the Lambda show entry/exit. - Did it find the
scheduled_jobsrow? If the Aurora row was deleted but the Scheduler entry wasn’t reconciled, the trigger Lambda will log “no matching job.” - Did Step Functions start? The trigger Lambda starts an execution; check the state machine’s execution history.
- Did AgentCore run? The Step Functions execution has the AgentCore invocation as a step; check its output.
Most missed runs are at step 1 or 2. Schedule drift between Aurora and Scheduler is the single most common class of bug, which is why the reconciler Lambda exists — but it’s reconciler-based (eventual), not transactional.
Debugging a trigger that didn’t fire
Section titled “Debugging a trigger that didn’t fire”For event-driven automations:
- Did the event arrive on the bus? EventBridge has a “Event history” view that shows every event received.
- Did the trigger rule match? Each rule’s metrics page shows how many events matched vs. invoked. A mismatch means the event pattern is wrong.
- Did the target Lambda succeed? Same chain as scheduled from here.
Known limits
Section titled “Known limits”- No backfill for missed scheduled runs. If AWS Scheduler failed to invoke your target for three days, those three runs are lost. The manager does not re-fire missed schedules.
- Trigger rules are tenant-scoped but event bus is deployment-scoped. All tenants in a deployment share one event bus. The rule’s own matcher enforces tenant scope.
- Provisioning is eventually consistent. A new schedule takes a few seconds after creation before AWS Scheduler actually has it. If you’re writing tests, don’t expect turn-by-turn sync.
- Maximum schedule rate is 1-per-minute. AWS Scheduler supports higher rates via batched invocation, but ThinkWork does not configure that path — fastest stable cadence is 1/min.
Related pages
Section titled “Related pages”- Automations (overview) — the umbrella
- Admin: Automations — the UI
- Threads: Lifecycle and Types — where AUTO threads fit
- Integrations — how integration events become triggers
Under the hood
Section titled “Under the hood”Schedules and triggers are first-class rows in the operational database, reconciled into AWS Scheduler and EventBridge by the scheduled-jobs handler in packages/api/src/handlers/scheduled-jobs.ts. When a schedule fires, AWS Scheduler invokes the trigger target; the trigger resolves the current schedule row at fire time and kicks off the Step Functions state machine.
State machines, rule patterns, and the EventBridge custom bus are provisioned by the ThinkWork Terraform module. For the operator view of schedules, see Admin: Automations.