1import * as Schema from "effect/Schema";
2
3import {
4 CommandId,
5 ForwardCompatibleArray,
6 IsoDateTime,
7 ProjectId,
8 ScheduledTaskId,
9 SecretRef,
10 ThreadId,
11 TrimmedNonEmptyString,
12} from "./baseSchemas.ts";
13import { ModelSelection } from "./modelSelection.ts";
14import {
15 OrchestrationV2Actor,
16 OrchestrationV2CreationSource,
17 OrchestrationV2ThreadLaunchWorkspaceStrategy,
18} from "./orchestrationV2.ts";
19import { ProviderInteractionMode, RuntimeMode } from "./providerPolicy.ts";
20
21/** 24-hour "HH:MM" wall-clock time. Mirrors `parseTimeOfDay` on the server. */
22const TimeOfDay = TrimmedNonEmptyString.check(
23 Schema.isPattern(/^([01]?\d|2[0-3]):([0-5]\d)$/),
24).annotate({ description: "Local wall-clock time in 24-hour HH:MM form, such as 09:30." });
25
26export const MIN_SCHEDULED_TASK_INTERVAL_MS = 60_000;
27
28const ScheduledTaskIntervalMs = Schema.Int.check(Schema.isGreaterThan(0)).annotate({
29 description: "Positive interval in milliseconds.",
30});
31
32const ScheduledTaskIntervalSchedule = Schema.Struct({
33 type: Schema.Literal("interval").annotate({
34 description: "Select interval scheduling.",
35 }),
36 everyMs: ScheduledTaskIntervalMs,
37}).annotate({
38 description: "Run repeatedly after a fixed number of milliseconds.",
39});
40
41const ScheduledTaskFixedTimeSchedule = Schema.Struct({
42 type: Schema.Literal("fixed_time").annotate({
43 description: "Select a fixed local wall-clock time.",
44 }),
45 timeOfDay: TimeOfDay,
46 weekdays: Schema.optional(
47 Schema.Array(
48 Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 6 })).annotate({
49 description: "Weekday number where 0 is Sunday and 6 is Saturday.",
50 }),
51 ).annotate({
52 description: "Optional weekdays; omit to run every day.",
53 }),
54 ),
55}).annotate({
56 description: "Run at a fixed local wall-clock time on selected weekdays.",
57});
58
59const ScheduledTaskWebhookSignatureFields = {
60 header: TrimmedNonEmptyString.annotate({
61 description: "Request header carrying the signature, such as x-hub-signature-256.",
62 }),
63 encoding: Schema.Literals(["hex", "base64"]).annotate({
64 description: "How the HMAC-SHA256 digest is encoded in the header.",
65 }),
66 prefix: Schema.String.annotate({
67 description: "Text before the digest in the header value, such as 'sha256='. Empty for none.",
68 }),
69};
70
71/** HMAC-SHA256 over the raw request body. The secret is never part of the read model. */
72export const ScheduledTaskWebhookSignature = Schema.Struct(
73 ScheduledTaskWebhookSignatureFields,
74).annotate({ description: "Optional HMAC-SHA256 signature check over the raw request body." });
75export type ScheduledTaskWebhookSignature = typeof ScheduledTaskWebhookSignature.Type;
76
77/** Matches how long the relay holds a request for an offline environment. */
78export const MAX_WEBHOOK_DELIVERY_AGE_MINUTES = 24 * 60;
79
80const WebhookMaxDeliveryAgeMinutes = Schema.Int.check(
81 Schema.isBetween({ minimum: 1, maximum: MAX_WEBHOOK_DELIVERY_AGE_MINUTES }),
82).annotate({
83 description:
84 "Skip requests the relay held longer than this many minutes while the environment was offline. Null runs every request.",
85});
86
87const ScheduledTaskWebhookSchedule = Schema.Struct({
88 type: Schema.Literal("webhook").annotate({
89 description: "Run when the task's webhook URL receives a request.",
90 }),
91 signature: Schema.NullOr(ScheduledTaskWebhookSignature),
92 // Optional so rows saved before this setting existed still decode.
93 maxDeliveryAgeMinutes: Schema.optional(Schema.NullOr(WebhookMaxDeliveryAgeMinutes)),
94}).annotate({
95 description:
96 "Run on each request to the task's webhook URL. The prompt may use {{body.path}}, {{headers.name}}, {{query.name}}, {{body}} and {{request}} placeholders.",
97});
98
99const ScheduledTaskUpsertWebhookSchedule = Schema.Struct({
100 type: Schema.Literal("webhook").annotate({
101 description: "Run when the task's webhook URL receives a request.",
102 }),
103 signature: Schema.optional(
104 Schema.NullOr(
105 Schema.Struct({
106 ...ScheduledTaskWebhookSignatureFields,
107 secret: Schema.optional(TrimmedNonEmptyString).annotate({
108 description: "Shared signing secret. Omit to keep the stored secret.",
109 }),
110 secretRef: Schema.optional(SecretRef).annotate({
111 description:
112 "A secret the user entered through request_secret, used instead of secret. It is consumed by this save.",
113 }),
114 }),
115 ),
116 ).annotate({
117 description: "Signature check; omit or null to accept requests by URL token only.",
118 }),
119 maxDeliveryAgeMinutes: Schema.optional(Schema.NullOr(WebhookMaxDeliveryAgeMinutes)),
120}).annotate({
121 description:
122 "Run on each request to the task's webhook URL. The prompt may use {{body.path}}, {{headers.name}}, {{query.name}}, {{body}} and {{request}} placeholders.",
123});
124
125/**
126 * Read model for persisted schedules. Keep accepting legacy sub-minute rows so
127 * users can list, disable, edit, or delete them after the write minimum changes.
128 */
129export const ScheduledTaskSchedule = Schema.Union([
130 ScheduledTaskIntervalSchedule,
131 ScheduledTaskFixedTimeSchedule,
132 ScheduledTaskWebhookSchedule,
133]).annotate({
134 description:
135 "Structured trigger. Pass an object with type 'interval', 'fixed_time' or 'webhook'.",
136});
137export type ScheduledTaskSchedule = typeof ScheduledTaskSchedule.Type;
138
139/** Mutation model: newly created or updated interval schedules run at most once per minute. */
140export const ScheduledTaskUpsertSchedule = Schema.Union([
141 Schema.Struct({
142 type: Schema.Literal("interval").annotate({
143 description: "Select interval scheduling.",
144 }),
145 everyMs: ScheduledTaskIntervalMs.check(
146 Schema.isGreaterThanOrEqualTo(MIN_SCHEDULED_TASK_INTERVAL_MS),
147 ).annotate({
148 description: "Interval in milliseconds, with a minimum of 60000 (one minute).",
149 }),
150 }).annotate({
151 description: "Run repeatedly after a fixed number of milliseconds.",
152 }),
153 ScheduledTaskFixedTimeSchedule,
154 ScheduledTaskUpsertWebhookSchedule,
155]).annotate({
156 description: "Writable trigger. Pass an object with type 'interval', 'fixed_time' or 'webhook'.",
157});
158export type ScheduledTaskUpsertSchedule = typeof ScheduledTaskUpsertSchedule.Type;
159
160export const ScheduledTaskRunStatus = Schema.Literals(["never", "running", "succeeded", "failed"]);
161export type ScheduledTaskRunStatus = typeof ScheduledTaskRunStatus.Type;
162
163/** Where a webhook task receives requests. Present only on webhook tasks. */
164export const ScheduledTaskWebhookEndpoint = Schema.Struct({
165 /** Environment-relative path including the secret token; works on any origin that reaches the environment. */
166 path: TrimmedNonEmptyString,
167 /** Public T3 Connect URL, or null when the environment is not linked to T3 Connect. */
168 url: Schema.NullOr(TrimmedNonEmptyString),
169 hasSecret: Schema.Boolean,
170});
171export type ScheduledTaskWebhookEndpoint = typeof ScheduledTaskWebhookEndpoint.Type;
172
173export const ScheduledTask = Schema.Struct({
174 id: ScheduledTaskId,
175 title: TrimmedNonEmptyString,
176 prompt: TrimmedNonEmptyString,
177 enabled: Schema.Boolean,
178 schedule: ScheduledTaskSchedule,
179 projectId: ProjectId,
180 threadId: Schema.NullOr(ThreadId),
181 workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy,
182 modelSelection: ModelSelection,
183 runtimeMode: RuntimeMode,
184 interactionMode: ProviderInteractionMode,
185 createdBy: OrchestrationV2Actor,
186 creationSource: OrchestrationV2CreationSource,
187 createdAt: IsoDateTime,
188 updatedAt: IsoDateTime,
189 nextRunAt: Schema.NullOr(IsoDateTime),
190 lastRunAt: Schema.NullOr(IsoDateTime),
191 lastRunStatus: ScheduledTaskRunStatus,
192 lastRunError: Schema.NullOr(Schema.String),
193 runCount: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
194 webhook: Schema.optional(ScheduledTaskWebhookEndpoint),
195});
196export type ScheduledTask = typeof ScheduledTask.Type;
197
198export const ScheduledTaskListInput = Schema.Struct({});
199export type ScheduledTaskListInput = typeof ScheduledTaskListInput.Type;
200
201export const ScheduledTaskListResult = Schema.Struct({
202 // Trigger types grow over time; a client must not lose the whole list over
203 // one task it cannot decode.
204 tasks: ForwardCompatibleArray(ScheduledTask),
205});
206export type ScheduledTaskListResult = typeof ScheduledTaskListResult.Type;
207
208export const ScheduledTaskUpsertInput = Schema.Struct({
209 id: Schema.optional(ScheduledTaskId),
210 requireExisting: Schema.optional(Schema.Boolean).annotate({
211 description: "Reject the save if the task no longer exists, for edits from a client form.",
212 }),
213 commandId: Schema.optional(CommandId),
214 title: TrimmedNonEmptyString,
215 prompt: TrimmedNonEmptyString,
216 enabled: Schema.Boolean,
217 schedule: ScheduledTaskUpsertSchedule,
218 projectId: ProjectId,
219 threadId: Schema.optional(Schema.NullOr(ThreadId)),
220 workspaceStrategy: OrchestrationV2ThreadLaunchWorkspaceStrategy,
221 modelSelection: ModelSelection,
222 runtimeMode: RuntimeMode,
223 interactionMode: ProviderInteractionMode,
224 createdBy: Schema.optional(OrchestrationV2Actor),
225 creationSource: Schema.optional(OrchestrationV2CreationSource),
226});
227export type ScheduledTaskUpsertInput = typeof ScheduledTaskUpsertInput.Type;
228
229/** Partial update that flips only the enabled flag — never overwrites other fields. */
230export const ScheduledTaskSetEnabledInput = Schema.Struct({
231 id: ScheduledTaskId,
232 enabled: Schema.Boolean,
233});
234export type ScheduledTaskSetEnabledInput = typeof ScheduledTaskSetEnabledInput.Type;
235
236export const ScheduledTaskDeleteInput = Schema.Struct({
237 id: ScheduledTaskId,
238});
239export type ScheduledTaskDeleteInput = typeof ScheduledTaskDeleteInput.Type;
240
241export const ScheduledTaskRunNowInput = Schema.Struct({
242 id: ScheduledTaskId,
243});
244export type ScheduledTaskRunNowInput = typeof ScheduledTaskRunNowInput.Type;
245
246export const ScheduledTaskRotateWebhookTokenInput = Schema.Struct({
247 id: ScheduledTaskId,
248});
249export type ScheduledTaskRotateWebhookTokenInput = typeof ScheduledTaskRotateWebhookTokenInput.Type;
250
251export const ScheduledTaskWebhookDeliveryId = TrimmedNonEmptyString.pipe(
252 Schema.brand("ScheduledTaskWebhookDeliveryId"),
253);
254export type ScheduledTaskWebhookDeliveryId = typeof ScheduledTaskWebhookDeliveryId.Type;
255
256export const ScheduledTaskWebhookDeliveryOutcome = Schema.Literals([
257 "accepted",
258 "dispatch_failed",
259 "rejected_signature",
260 "disabled",
261 "rate_limited",
262 "expired",
263]);
264export type ScheduledTaskWebhookDeliveryOutcome = typeof ScheduledTaskWebhookDeliveryOutcome.Type;
265
266export const ScheduledTaskWebhookDeliverySummary = Schema.Struct({
267 id: ScheduledTaskWebhookDeliveryId,
268 taskId: ScheduledTaskId,
269 receivedAt: IsoDateTime,
270 method: TrimmedNonEmptyString,
271 contentType: Schema.NullOr(Schema.String),
272 bodyBytes: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)),
273 outcome: ScheduledTaskWebhookDeliveryOutcome,
274 /** True when a configured signature matched; false when none was configured. */
275 signatureVerified: Schema.Boolean,
276 /** Template placeholders that had no value in this request and rendered empty. */
277 missingFields: Schema.Array(Schema.String),
278 error: Schema.NullOr(Schema.String),
279});
280export type ScheduledTaskWebhookDeliverySummary = typeof ScheduledTaskWebhookDeliverySummary.Type;
281
282export const ScheduledTaskWebhookDelivery = Schema.Struct({
283 ...ScheduledTaskWebhookDeliverySummary.fields,
284 query: Schema.String,
285 headers: Schema.Record(Schema.String, Schema.String),
286 /** Body as UTF-8 text, cut at the log limit; see bodyTruncated. */
287 body: Schema.String,
288 bodyTruncated: Schema.Boolean,
289 renderedPrompt: Schema.NullOr(Schema.String),
290});
291export type ScheduledTaskWebhookDelivery = typeof ScheduledTaskWebhookDelivery.Type;
292
293export const ScheduledTaskListWebhookDeliveriesInput = Schema.Struct({
294 id: ScheduledTaskId,
295});
296export type ScheduledTaskListWebhookDeliveriesInput =
297 typeof ScheduledTaskListWebhookDeliveriesInput.Type;
298
299export const ScheduledTaskListWebhookDeliveriesResult = Schema.Struct({
300 deliveries: Schema.Array(ScheduledTaskWebhookDeliverySummary),
301});
302export type ScheduledTaskListWebhookDeliveriesResult =
303 typeof ScheduledTaskListWebhookDeliveriesResult.Type;
304
305export const ScheduledTaskGetWebhookDeliveryInput = Schema.Struct({
306 id: ScheduledTaskId,
307 deliveryId: ScheduledTaskWebhookDeliveryId,
308});
309export type ScheduledTaskGetWebhookDeliveryInput = typeof ScheduledTaskGetWebhookDeliveryInput.Type;
310
311export const ScheduledTaskGetWebhookDeliveryResult = Schema.Struct({
312 delivery: ScheduledTaskWebhookDelivery,
313});
314export type ScheduledTaskGetWebhookDeliveryResult =
315 typeof ScheduledTaskGetWebhookDeliveryResult.Type;
316
317export const ScheduledTaskMutationResult = Schema.Struct({
318 task: ScheduledTask,
319});
320export type ScheduledTaskMutationResult = typeof ScheduledTaskMutationResult.Type;
321
322export const ScheduledTaskDeleteResult = Schema.Struct({
323 id: ScheduledTaskId,
324});
325export type ScheduledTaskDeleteResult = typeof ScheduledTaskDeleteResult.Type;
326
327export const ScheduledTaskRunNowResult = Schema.Struct({
328 task: ScheduledTask,
329});
330export type ScheduledTaskRunNowResult = typeof ScheduledTaskRunNowResult.Type;
331
332export class ScheduledTaskError extends Schema.TaggedError<ScheduledTaskError>()(
333 "ScheduledTaskError",
334 {
335 message: Schema.String,
336 taskId: Schema.optional(ScheduledTaskId),
337 cause: Schema.optional(Schema.Defect()),
338 },
339) {}