Skip to main content

Trigger Management and Revision History

Flyte manages triggers using a dual-table architecture that separates the current state from historical changes. This design ensures that every modification—whether it's a deployment, a state toggle, or a deletion—is recorded in an append-only history for auditability and supports optimistic locking to prevent concurrent update conflicts.

The system relies on the TriggerRepo interface (found in runs/repository/interfaces/trigger.go) to coordinate updates between the triggers table (latest state) and the trigger_revisions table (immutable history).

Deploying and Updating Triggers

When you deploy a trigger, you must account for potential concurrent modifications. If two users attempt to update the same trigger simultaneously, the system uses optimistic locking to ensure only one succeeds.

To deploy or update a trigger, use the SaveTrigger method. You must provide the expectedRevision which represents the version of the trigger you last read.

// Example from runs/service/trigger_service.go
func (s *triggerService) DeployTrigger(
ctx context.Context,
req *connect.Request[triggerpb.DeployTriggerRequest],
) (*connect.Response[triggerpb.DeployTriggerResponse], error) {
request := req.Msg

// ... model transformation ...

// request.GetRevision() provides the optimistic lock version
saved, err := s.db.TriggerRepo().SaveTrigger(ctx, triggerModel, request.GetRevision())
if err != nil {
// If the revision in the DB doesn't match request.GetRevision(),
// SaveTrigger returns an "optimistic lock failure" error.
return nil, connect.NewError(connect.CodeInternal, err)
}

return connect.NewResponse(&triggerpb.DeployTriggerResponse{
Trigger: saved.ToProto(),
}), nil
}

How Optimistic Locking Works Internally

Internally, SaveTrigger (implemented in runs/repository/impl/trigger.go) executes a PostgreSQL INSERT ... ON CONFLICT statement. If an expectedRevision greater than zero is provided, it appends a WHERE clause to the update:

// From runs/repository/impl/trigger.go
if expectedRevision > 0 {
suffix += fmt.Sprintf(" WHERE triggers.latest_revision = %d", expectedRevision)
}

If the latest_revision in the database has changed since you last fetched the trigger, the query affects zero rows, and the repository returns a specific error: optimistic lock failure: trigger was modified concurrently, please fetch latest and retry.

Managing Trigger State and Deletion

Triggers can be activated, deactivated, or deleted. These operations are performed in bulk and always result in a new entry in the revision history.

Activating and Deactivating

The UpdateTriggers method toggles the active status of multiple triggers. This is useful for pausing automations without deleting their configuration.

keys := []interfaces.TriggerNameKey{
{Project: "p1", Domain: "d1", TaskName: "t1", Name: "my-trigger"},
}
err := triggerRepo.UpdateTriggers(ctx, keys, false) // Deactivate

Soft Deletion

Flyte does not perform hard deletes on triggers. Instead, DeleteTriggers performs a soft delete by setting the deleted_at timestamp and marking the trigger as active = false. This preserves the trigger's history while removing it from active scheduling.

err := triggerRepo.DeleteTriggers(ctx, keys)

Both UpdateTriggers and DeleteTriggers increment the latest_revision and append a new row to trigger_revisions with the corresponding action type (e.g., TRIGGER_REVISION_ACTION_ACTIVATE, TRIGGER_REVISION_ACTION_DELETE).

Auditing Revision History

Because Flyte maintains an append-only history, you can retrieve the full audit trail of any trigger. This is critical for debugging why a trigger fired or who changed its configuration.

Use ListTriggerRevisions to fetch the history, which returns models.TriggerRevision objects ordered by revision descending (newest first).

// Example from runs/service/trigger_service.go
revisions, err := s.db.TriggerRepo().ListTriggerRevisions(ctx,
project, domain, taskName, triggerName, listInput)

Each revision captures a snapshot of the trigger's spec, automation_spec, and metadata at the time of the change, along with the action that created the revision.

Maintaining Task Consistency

Flyte denormalizes certain trigger metadata onto the tasks table to optimize query performance for task listings. The TriggerRepo ensures this metadata stays in sync automatically.

Whenever a trigger is saved, updated, or deleted, the repository invokes refreshTaskTriggerMeta within the same database transaction. This function recomputes the following fields on the tasks table:

  • total_triggers: The count of all non-deleted triggers for the task version.
  • active_triggers: The count of triggers currently marked as active.
  • trigger_name: The name of the trigger (populated only if exactly one trigger exists).
  • trigger_automation_spec: The automation configuration (populated only if exactly one trigger exists).

Because this logic is embedded in the TriggerRepo implementation, you should always use the repository methods rather than manual SQL updates to ensure the tasks table remains consistent with the triggers table.