Scheduled and Event-driven
ThinkWork has two entry paths for background work: a schedule fires at a
predetermined time, or an event arrives and triggers a run. Both surface as an
Automation — a trigger (schedule or webhook) bound to a target. Explicit
multi-step orchestration surfaces as a Workflow, which can itself be an
Automation’s target. AWS Scheduler, EventBridge, and scheduled_jobs are the
plumbing that wakes those product records.
Routing both entry points through durable ThinkWork records is what gives the Traceability operating guarantee its consistency: a scheduled monthly check-in and an event-triggered triage flow both have tenant-scoped rows, cost/audit accounting, and operator-visible evidence.
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 Automation, Workflow, or internal job the schedule should wake.
- A start and optional end window.
- A tenant + Space scope — schedules are tenant-scoped and usually run in a specific Space.
Automation schedules are created through Settings -> Automations. Lower-level schedule rows can still be managed by operator/admin surfaces or GraphQL when a feature owns the target contract.
Event-driven
Section titled “Event-driven”An event-driven trigger fires when something external happens. The shipped event trigger is the webhook: an inbound HTTP POST to an endpoint the Automation mints inline (URL + token), dispatched through the same shared automation dispatcher as schedule and manual runs. The raw JSON body becomes the run input — passed to routine/workflow targets, or appended to an agent-thread target’s instruction context inside an explicit untrusted-data fence. There is no separate Webhooks settings page; a webhook is simply an Automation’s trigger.
Richer event sources — matching on an EventBridge custom bus by detail-type,
source, and detail — remain later-phase work. When they arrive, EventBridge
or a workflow engine such as n8n may trigger or participate in a run, but
ThinkWork still owns the Automation identity, run state, and dispatch ledger.
Candidate sources for that phase:
- 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).
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 scheduled Automation or schedule-backed Workflow │ GraphQL product mutation ▼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 an Automation run OR │ starts a Workflow SFN execution) ▼For agent-thread targets: automation run -> agent wakeup -> AgentCoreFor routine/workflow targets: automation run -> Routine adapter -> SFN.StartExecutionThe chain exists because schedule plumbing is stored in ThinkWork
(scheduled_jobs in Aurora), not as bare AWS Scheduler entries. The manager
Lambda keeps Aurora and AWS Scheduler in sync. The product identity still lives
above that plumbing: the Automation owns its trigger, target, run-as user, and
run ledger; Workflow owns orchestration state/evidence.
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:
- Does an Automation run or Workflow run exist? If yes, debug the product run first.
- 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 was not reconciled, the trigger Lambda will log “no matching job.” - Did AgentCore or Step Functions run? Agent-thread automations dispatch an AgentCore agent wakeup; routine/workflow targets start Step Functions.
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 triggers:
- 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 - the v1 automation product object
- Admin: Automations - the operator surface for prompt-first Automations
- 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 operational rows in the 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 starts the owning product path.
State machines, rule patterns, and the EventBridge custom bus are provisioned by the ThinkWork Terraform module. For recurring or webhook-triggered agent work, use Settings → Automations.