diff --git a/src/Trax.Effect.Data.Postgres/Migrations/023_once_schedule_type.sql b/src/Trax.Effect.Data.Postgres/Migrations/023_once_schedule_type.sql
new file mode 100644
index 0000000..b26af3b
--- /dev/null
+++ b/src/Trax.Effect.Data.Postgres/Migrations/023_once_schedule_type.sql
@@ -0,0 +1,18 @@
+-- Add 'once' value to schedule_type enum for one-off delayed jobs
+ALTER TYPE trax.schedule_type ADD VALUE IF NOT EXISTS 'once';
+
+-- Add scheduled_at column to manifest (used by Once schedule type)
+ALTER TABLE trax.manifest
+ ADD COLUMN IF NOT EXISTS scheduled_at timestamptz;
+
+-- Add scheduled_at column to work_queue (used by delayed triggers)
+ALTER TABLE trax.work_queue
+ ADD COLUMN IF NOT EXISTS scheduled_at timestamptz;
+
+-- Index for efficiently finding work queue entries with future scheduled_at.
+-- Note: no partial index on manifest for schedule_type='once' here because
+-- new enum values cannot be referenced in the same transaction they are added.
+-- The existing ix_manifest_scheduling index covers enabled manifests.
+CREATE INDEX IF NOT EXISTS ix_work_queue_scheduled_at
+ ON trax.work_queue (scheduled_at)
+ WHERE status = 'queued' AND scheduled_at IS NOT NULL;
diff --git a/src/Trax.Effect/Enums/ScheduleType.cs b/src/Trax.Effect/Enums/ScheduleType.cs
index 260e5ba..7c7dc4a 100644
--- a/src/Trax.Effect/Enums/ScheduleType.cs
+++ b/src/Trax.Effect/Enums/ScheduleType.cs
@@ -80,4 +80,14 @@ public enum ScheduleType
/// which dependents fire and with what input.
///
DormantDependent = 5,
+
+ ///
+ /// The manifest fires exactly once at a scheduled time, then auto-disables.
+ ///
+ ///
+ /// Use this for delayed one-off jobs (e.g., "send reminder in 30 minutes").
+ /// The property must be set.
+ /// After successful execution, the manifest is automatically disabled by the scheduler.
+ ///
+ Once = 6,
}
diff --git a/src/Trax.Effect/Models/Manifest/DTOs/CreateManifest.cs b/src/Trax.Effect/Models/Manifest/DTOs/CreateManifest.cs
index 3d41911..23ca806 100644
--- a/src/Trax.Effect/Models/Manifest/DTOs/CreateManifest.cs
+++ b/src/Trax.Effect/Models/Manifest/DTOs/CreateManifest.cs
@@ -70,5 +70,11 @@ public class CreateManifest
///
public int? MisfireThresholdSeconds { get; set; }
+ ///
+ /// The earliest time this manifest should be executed.
+ /// Used with for delayed one-off jobs.
+ ///
+ public DateTime? ScheduledAt { get; set; }
+
#endregion
}
diff --git a/src/Trax.Effect/Models/Manifest/Manifest.cs b/src/Trax.Effect/Models/Manifest/Manifest.cs
index 6c21d1d..e033357 100644
--- a/src/Trax.Effect/Models/Manifest/Manifest.cs
+++ b/src/Trax.Effect/Models/Manifest/Manifest.cs
@@ -173,6 +173,18 @@ public class Manifest : IModel
[Column("misfire_threshold_seconds")]
public int? MisfireThresholdSeconds { get; set; }
+ ///
+ /// Gets or sets the earliest time this manifest should be executed.
+ ///
+ ///
+ /// Used with to define when the one-off job should fire.
+ /// The ManifestManager will queue a work queue entry when ScheduledAt <= now
+ /// and is null. After successful execution, the manifest
+ /// is automatically disabled.
+ ///
+ [Column("scheduled_at")]
+ public DateTime? ScheduledAt { get; set; }
+
#endregion
#endregion
@@ -235,6 +247,7 @@ public static Manifest Create(CreateManifest manifest)
Priority = manifest.Priority,
MisfirePolicy = manifest.MisfirePolicy,
MisfireThresholdSeconds = manifest.MisfireThresholdSeconds,
+ ScheduledAt = manifest.ScheduledAt,
};
if (manifest.Properties != null)
diff --git a/src/Trax.Effect/Models/WorkQueue/DTOs/CreateWorkQueue.cs b/src/Trax.Effect/Models/WorkQueue/DTOs/CreateWorkQueue.cs
index bf3f686..0f53b9d 100644
--- a/src/Trax.Effect/Models/WorkQueue/DTOs/CreateWorkQueue.cs
+++ b/src/Trax.Effect/Models/WorkQueue/DTOs/CreateWorkQueue.cs
@@ -29,4 +29,10 @@ public class CreateWorkQueue
/// Dispatch priority. Range: 0-31 (clamped in WorkQueue.Create). Defaults to 0.
///
public int Priority { get; set; }
+
+ ///
+ /// The earliest time this work queue entry should be dispatched.
+ /// Null means dispatch immediately.
+ ///
+ public DateTime? ScheduledAt { get; set; }
}
diff --git a/src/Trax.Effect/Models/WorkQueue/WorkQueue.cs b/src/Trax.Effect/Models/WorkQueue/WorkQueue.cs
index 0979d2a..91f94ad 100644
--- a/src/Trax.Effect/Models/WorkQueue/WorkQueue.cs
+++ b/src/Trax.Effect/Models/WorkQueue/WorkQueue.cs
@@ -65,6 +65,17 @@ public class WorkQueue : IModel
[Column("dispatched_at")]
public DateTime? DispatchedAt { get; set; }
+ ///
+ /// The earliest time this work queue entry should be dispatched.
+ ///
+ ///
+ /// When set, the JobDispatcher will skip this entry until ScheduledAt <= now.
+ /// Used by TriggerAsync(externalId, delay) to create delayed triggers without
+ /// affecting the manifest's normal schedule. Null means dispatch immediately.
+ ///
+ [Column("scheduled_at")]
+ public DateTime? ScheduledAt { get; set; }
+
///
/// Dispatch priority for this entry. Higher values (up to 31) are dispatched first.
///
@@ -114,6 +125,7 @@ public static WorkQueue Create(CreateWorkQueue dto)
InputTypeName = dto.InputTypeName,
ManifestId = dto.ManifestId,
Priority = Math.Clamp(dto.Priority, MinPriority, MaxPriority),
+ ScheduledAt = dto.ScheduledAt,
Status = WorkQueueStatus.Queued,
CreatedAt = DateTime.UtcNow,
};