Skip to content

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.

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.

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 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 -> AgentCore
For routine/workflow targets: automation run -> Routine adapter -> SFN.StartExecution

The 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.

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.

When a scheduled automation doesn’t run, work the chain:

  1. Does an Automation run or Workflow run exist? If yes, debug the product run first.
  2. 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.
  3. Did job-trigger receive the invocation? CloudWatch logs for the Lambda show entry/exit.
  4. Did it find the scheduled_jobs row? If the Aurora row was deleted but the Scheduler entry was not reconciled, the trigger Lambda will log “no matching job.”
  5. 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.

For event-driven triggers:

  1. Did the event arrive on the bus? EventBridge has a “Event history” view that shows every event received.
  2. 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.
  3. Did the target Lambda succeed? Same chain as scheduled from here.
  • 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.

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.