@redflow/client 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,280 @@
1
+ # redflow
2
+
3
+ Redis-backed workflow runtime for Bun.
4
+
5
+ ## Warning
6
+
7
+ This project is still in early alpha stage.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ bun add @redflow/client -E
13
+ ```
14
+
15
+ ## Most users start here: `defineWorkflow`
16
+
17
+ ```ts
18
+ import { defineWorkflow } from "@redflow/client";
19
+ import { z } from "zod";
20
+
21
+ export const sendWelcomeEmail = defineWorkflow(
22
+ "send-welcome-email",
23
+ {
24
+ schema: z.object({ userId: z.string().min(1) })
25
+ },
26
+ async ({ input, step }) => {
27
+ const user = await step.run({ name: "fetch-user" }, async () => {
28
+ return { id: input.userId, email: "test@example.com" };
29
+ });
30
+
31
+ await step.run({ name: "send-email" }, async () => {
32
+ return { ok: true, to: user.email };
33
+ });
34
+
35
+ return { sent: true };
36
+ },
37
+ );
38
+ ```
39
+
40
+ Handler context gives you:
41
+
42
+ - `input` — validated input (if `schema` is provided)
43
+ - `run` — run metadata (`id`, `workflow`, `queue`, `attempt`, `maxAttempts`)
44
+ - `signal` — cancellation signal
45
+ - `step` — durable step API
46
+
47
+ ## Step API (inside workflow handlers)
48
+
49
+ ### 1) `step.run`
50
+
51
+ Use for durable, cached units of work.
52
+
53
+ ```ts
54
+ const payment = await step.run(
55
+ { name: "capture-payment", timeoutMs: 4_000 },
56
+ async ({ signal }) => {
57
+ // pass signal to external APIs when supported
58
+ return { chargeId: "ch_123" };
59
+ },
60
+ );
61
+ ```
62
+
63
+ ### 2) `step.runWorkflow`
64
+
65
+ Use when parent workflow must wait for child workflow output.
66
+
67
+ ```ts
68
+ const receipt = await step.runWorkflow(
69
+ {
70
+ name: "send-receipt",
71
+ timeoutMs: 20_000,
72
+ runAt: new Date(Date.now() + 500),
73
+ idempotencyTtl: 60 * 60 * 24,
74
+ idempotencyKey: `receipt:${input.orderId}`,
75
+ },
76
+ sendReceiptWorkflow,
77
+ {
78
+ orderId: input.orderId,
79
+ email: input.email,
80
+ totalCents: input.totalCents,
81
+ },
82
+ );
83
+ ```
84
+
85
+ `step.runWorkflow` waits for child completion until the step is canceled.
86
+ Set `timeoutMs` to cap total waiting time.
87
+
88
+ ### 3) `step.emitWorkflow`
89
+
90
+ Use to trigger child workflow and keep only child run id.
91
+
92
+ ```ts
93
+ const analyticsRunId = await step.emitWorkflow(
94
+ {
95
+ name: "emit-analytics",
96
+ runAt: new Date(Date.now() + 2_000),
97
+ idempotencyTtl: 60 * 60,
98
+ idempotencyKey: `analytics:${input.orderId}`,
99
+ },
100
+ analyticsWorkflow,
101
+ {
102
+ orderId: input.orderId,
103
+ totalCents: input.totalCents,
104
+ },
105
+ );
106
+ ```
107
+
108
+ ## Run workflows
109
+
110
+ The object returned by `defineWorkflow(...)` has `.run(...)`.
111
+
112
+ ```ts
113
+ const handle = await sendWelcomeEmail.run(
114
+ { userId: "user_123" },
115
+ {
116
+ idempotencyKey: "welcome:user_123",
117
+ idempotencyTtl: 60 * 60,
118
+ },
119
+ );
120
+
121
+ const output = await handle.result({ timeoutMs: 15_000 });
122
+ ```
123
+
124
+ Delayed run:
125
+
126
+ ```ts
127
+ const handle = await sendWelcomeEmail.run(
128
+ { userId: "user_789" },
129
+ {
130
+ runAt: new Date(Date.now() + 60_000),
131
+ idempotencyKey: "welcome:user_789:delayed",
132
+ },
133
+ );
134
+
135
+ const output = await handle.result({ timeoutMs: 90_000 });
136
+ ```
137
+
138
+ ## Start a worker
139
+
140
+ Import workflows, then run `startWorker()`.
141
+
142
+ ```ts
143
+ import { startWorker } from "@redflow/client";
144
+ import "./workflows";
145
+
146
+ const worker = await startWorker({
147
+ url: process.env.REDIS_URL,
148
+ prefix: "redflow:prod",
149
+ concurrency: 4,
150
+ });
151
+ ```
152
+
153
+ Explicit queues + runtime tuning:
154
+
155
+ ```ts
156
+ const worker = await startWorker({
157
+ url: process.env.REDIS_URL,
158
+ prefix: "redflow:prod",
159
+ queues: ["critical", "io", "analytics"],
160
+ concurrency: 8,
161
+ runtime: {
162
+ leaseMs: 5000,
163
+ blmoveTimeoutSec: 1,
164
+ reaperIntervalMs: 500,
165
+ },
166
+ });
167
+ ```
168
+
169
+ ## Workflow options examples
170
+
171
+ ### Cron
172
+
173
+ ```ts
174
+ defineWorkflow(
175
+ "digest-cron",
176
+ {
177
+ queue: "ops",
178
+ cron: [
179
+ { id: "digest-10s", expression: "*/10 * * * * *" },
180
+ { expression: "0 */5 * * * *", timezone: "UTC", input: { source: "cron" } },
181
+ ],
182
+ },
183
+ async ({ input }) => ({ tick: true, input }),
184
+ );
185
+ ```
186
+
187
+ ### onFailure
188
+
189
+ ```ts
190
+ import { NonRetriableError } from "@redflow/client";
191
+
192
+ defineWorkflow(
193
+ "invoice-sync",
194
+ {
195
+ queue: "billing",
196
+ retries: { maxAttempts: 4 },
197
+ onFailure: async ({ error, run }) => {
198
+ console.error("workflow failed", run.id, run.workflow, error);
199
+ },
200
+ },
201
+ async () => {
202
+ throw new NonRetriableError("invoice not found in upstream system");
203
+ },
204
+ );
205
+ ```
206
+
207
+ ## Client APIs (advanced)
208
+
209
+ Use this when you need manual triggering, inspection, registry sync, or custom client setup.
210
+
211
+ ### Setup
212
+
213
+ ```ts
214
+ import { createClient, setDefaultClient } from "@redflow/client";
215
+
216
+ const client = createClient({
217
+ url: process.env.REDIS_URL,
218
+ prefix: "redflow:prod",
219
+ });
220
+
221
+ setDefaultClient(client);
222
+ ```
223
+
224
+ ### Trigger by workflow name
225
+
226
+ ```ts
227
+ const h1 = await client.runByName(
228
+ "checkout",
229
+ { orderId: "ord_2", totalCents: 1999 },
230
+ {
231
+ queueOverride: "critical",
232
+ idempotencyKey: "checkout:ord_2",
233
+ },
234
+ );
235
+
236
+ const h2 = await client.emitWorkflow(
237
+ "send-welcome-email",
238
+ { userId: "user_456" },
239
+ { idempotencyKey: "welcome:user_456" },
240
+ );
241
+ ```
242
+
243
+ ### Inspect and control runs
244
+
245
+ ```ts
246
+ const run = await client.getRun("run_123");
247
+ const steps = await client.getRunSteps("run_123");
248
+
249
+ const recent = await client.listRuns({ limit: 50 });
250
+ const failedCheckout = await client.listRuns({
251
+ workflow: "checkout",
252
+ status: "failed",
253
+ limit: 20,
254
+ });
255
+
256
+ const workflows = await client.listWorkflows();
257
+ const checkoutMeta = await client.getWorkflowMeta("checkout");
258
+
259
+ const canceled = await client.cancelRun("run_123", { reason: "requested by user" });
260
+ ```
261
+
262
+ ### RunHandle
263
+
264
+ ```ts
265
+ const handle = await client.emitWorkflow("checkout", { orderId: "ord_3", totalCents: 2999 });
266
+
267
+ const state = await handle.getState();
268
+ console.log(state?.status);
269
+
270
+ const output = await handle.result({ timeoutMs: 30_000 });
271
+ console.log(output);
272
+ ```
273
+
274
+ ### Registry sync ownership
275
+
276
+ ```ts
277
+ import { getDefaultRegistry } from "@redflow/client";
278
+
279
+ await client.syncRegistry(getDefaultRegistry(), { owner: "billing-service" });
280
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redflow/client",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "module": "src/index.ts",
package/src/client.ts CHANGED
@@ -17,12 +17,11 @@ import { safeJsonParse, safeJsonStringify, safeJsonTryParse } from "./internal/j
17
17
  import { nowMs } from "./internal/time";
18
18
  import { sleep } from "./internal/sleep";
19
19
  import type {
20
- EmitEventOptions,
20
+ EmitWorkflowOptions,
21
21
  ListedRun,
22
22
  ListRunsParams,
23
23
  RunHandle,
24
24
  RunOptions,
25
- ScheduleEventOptions,
26
25
  RunState,
27
26
  RunStatus,
28
27
  StepState,
@@ -50,6 +49,7 @@ export function defaultPrefix(): string {
50
49
 
51
50
  const STALE_WORKFLOW_GRACE_MS = 30_000;
52
51
  const IDEMPOTENCY_TTL_SEC = 60 * 60 * 24 * 7;
52
+ const POLL_MS = 250;
53
53
 
54
54
  const ENQUEUE_RUN_LUA = `
55
55
  local runId = ARGV[1]
@@ -249,10 +249,6 @@ function encodeCompositePart(value: string): string {
249
249
  return `${value.length}:${value}`;
250
250
  }
251
251
 
252
- function eventScopedIdempotencyKey(eventName: string, idempotencyKey: string): string {
253
- return `event:${encodeCompositePart(eventName)}:${encodeCompositePart(idempotencyKey)}`;
254
- }
255
-
256
252
  function defaultRegistryOwner(): string {
257
253
  const envOwner = process.env.REDFLOW_SYNC_OWNER?.trim();
258
254
  if (envOwner) return envOwner;
@@ -356,70 +352,35 @@ export class RedflowClient {
356
352
  const data = await this.redis.hgetall(keys.workflow(this.prefix, name));
357
353
  if (!data || Object.keys(data).length === 0) return null;
358
354
 
359
- const triggers = safeJsonTryParse<any>(data.triggersJson ?? null) as any;
355
+ const cron = safeJsonTryParse<any>(data.cronJson ?? null) as any;
360
356
  const retries = safeJsonTryParse<any>(data.retriesJson ?? null) as any;
361
357
  const updatedAt = Number(data.updatedAt ?? "0");
362
358
  const queue = data.queue ?? "default";
363
359
  return {
364
360
  name,
365
361
  queue,
366
- triggers,
362
+ cron: Array.isArray(cron) && cron.length > 0 ? cron : undefined,
367
363
  retries,
368
364
  updatedAt,
369
365
  };
370
366
  }
371
367
 
372
- async emitEvent(name: string, payload: unknown, options?: EmitEventOptions): Promise<string[]> {
373
- return await this.dispatchEvent(name, payload, {
368
+ /**
369
+ * Trigger a workflow by name. This is a direct run, not a fan-out.
370
+ * Use this when you want to start a workflow from outside a handler.
371
+ */
372
+ async emitWorkflow<TOutput = unknown>(
373
+ workflowName: string,
374
+ input: unknown,
375
+ options?: EmitWorkflowOptions,
376
+ ): Promise<RunHandle<TOutput>> {
377
+ return await this.runByName<TOutput>(workflowName, input, {
374
378
  idempotencyKey: options?.idempotencyKey,
375
379
  idempotencyTtl: options?.idempotencyTtl,
380
+ runAt: options?.runAt,
376
381
  });
377
382
  }
378
383
 
379
- async scheduleEvent(name: string, payload: unknown, options: ScheduleEventOptions): Promise<string[]> {
380
- if (!options || !isValidDate(options.availableAt)) {
381
- throw new Error("Invalid 'availableAt' for scheduleEvent");
382
- }
383
-
384
- return await this.dispatchEvent(name, payload, {
385
- idempotencyKey: options.idempotencyKey,
386
- idempotencyTtl: options.idempotencyTtl,
387
- availableAt: options.availableAt,
388
- });
389
- }
390
-
391
- private async dispatchEvent(
392
- name: string,
393
- payload: unknown,
394
- options: { idempotencyKey?: string; idempotencyTtl?: number; availableAt?: Date },
395
- ): Promise<string[]> {
396
- const subsKey = keys.eventWorkflows(this.prefix, name);
397
- const workflowNames = await this.redis.smembers(subsKey);
398
- const runIds: string[] = [];
399
-
400
- for (const workflowName of workflowNames) {
401
- const idempotencyKey = options.idempotencyKey ? eventScopedIdempotencyKey(name, options.idempotencyKey) : undefined;
402
- try {
403
- const handle = await this.runByName(workflowName, payload, {
404
- idempotencyKey,
405
- idempotencyTtl: options.idempotencyTtl,
406
- availableAt: options.availableAt,
407
- });
408
- runIds.push(handle.id);
409
- } catch (err) {
410
- if (err instanceof UnknownWorkflowError) {
411
- // Stale event subscriptions can remain briefly across deployments.
412
- // Drop them lazily so valid subscribers still receive events.
413
- await this.redis.srem(subsKey, workflowName);
414
- continue;
415
- }
416
- throw err;
417
- }
418
- }
419
-
420
- return runIds;
421
- }
422
-
423
384
  async runByName<TOutput = unknown>(workflowName: string, input: unknown, options?: RunOptions): Promise<RunHandle<TOutput>> {
424
385
  const queueFromRegistry = await this.getQueueForWorkflow(workflowName);
425
386
  const queue = options?.queueOverride ?? queueFromRegistry;
@@ -440,7 +401,7 @@ export class RedflowClient {
440
401
  workflowName,
441
402
  queue,
442
403
  input,
443
- availableAt: options?.availableAt,
404
+ availableAt: options?.runAt,
444
405
  idempotencyKey: options?.idempotencyKey,
445
406
  idempotencyTtl: options?.idempotencyTtl,
446
407
  maxAttempts,
@@ -606,12 +567,11 @@ export class RedflowClient {
606
567
 
607
568
  async waitForResult<TOutput = unknown>(
608
569
  runId: string,
609
- options?: { timeoutMs?: number; pollMs?: number },
570
+ options?: { timeoutMs?: number },
610
571
  ): Promise<TOutput> {
611
572
  const timeoutMs = options?.timeoutMs ?? 30_000;
612
- const pollMs = options?.pollMs ?? 250;
613
573
  const deadline = nowMs() + timeoutMs;
614
- const missingGraceMs = Math.max(250, Math.min(2000, pollMs * 4));
574
+ const missingGraceMs = Math.max(250, Math.min(2000, POLL_MS * 4));
615
575
 
616
576
  let missingSince: number | null = null;
617
577
  let seenState = false;
@@ -630,7 +590,7 @@ export class RedflowClient {
630
590
  }
631
591
 
632
592
  if (t > deadline) throw new TimeoutError(`Timed out waiting for run result (${runId})`);
633
- await sleep(pollMs);
593
+ await sleep(POLL_MS);
634
594
  continue;
635
595
  }
636
596
 
@@ -642,7 +602,7 @@ export class RedflowClient {
642
602
  if (state.status === "canceled") throw new CanceledError(state.cancelReason ? `Run canceled: ${state.cancelReason}` : "Run canceled");
643
603
 
644
604
  if (nowMs() > deadline) throw new TimeoutError(`Timed out waiting for run result (${runId})`);
645
- await sleep(pollMs);
605
+ await sleep(POLL_MS);
646
606
  }
647
607
  }
648
608
 
@@ -657,39 +617,24 @@ export class RedflowClient {
657
617
  for (const def of defs) {
658
618
  const name = def.options.name;
659
619
  const queue = def.options.queue ?? "default";
660
- const triggers = def.options.triggers ?? {};
620
+ const cron = def.options.cron ?? [];
661
621
  const retries = def.options.retries ?? {};
662
622
  const updatedAt = nowMs();
663
623
 
664
624
  const customCronIds = new Set<string>();
665
- for (const cron of triggers.cron ?? []) {
666
- if (!cron.id) continue;
667
- if (customCronIds.has(cron.id)) {
668
- throw new Error(`Duplicate cron trigger id '${cron.id}' in workflow '${name}'`);
625
+ for (const c of cron) {
626
+ if (!c.id) continue;
627
+ if (customCronIds.has(c.id)) {
628
+ throw new Error(`Duplicate cron trigger id '${c.id}' in workflow '${name}'`);
669
629
  }
670
- customCronIds.add(cron.id);
630
+ customCronIds.add(c.id);
671
631
  }
672
632
 
673
633
  const workflowKey = keys.workflow(this.prefix, name);
674
634
  await this.redis.sadd(keys.workflows(this.prefix), name);
675
635
 
676
- const prevEvents = safeJsonTryParse<string[]>(await this.redis.hget(workflowKey, "eventsJson")) ?? [];
677
- const nextEvents = Array.from(new Set(triggers.events ?? [])).sort();
678
-
679
- for (const ev of prevEvents) {
680
- if (!nextEvents.includes(ev)) {
681
- await this.redis.srem(keys.eventWorkflows(this.prefix, ev), name);
682
- }
683
- }
684
-
685
- for (const ev of nextEvents) {
686
- if (!prevEvents.includes(ev)) {
687
- await this.redis.sadd(keys.eventWorkflows(this.prefix, ev), name);
688
- }
689
- }
690
-
691
636
  const prevCronIds = safeJsonTryParse<string[]>(await this.redis.hget(workflowKey, "cronIdsJson")) ?? [];
692
- const nextCronIds = (triggers.cron ?? []).map((c) => this.computeCronId(name, c.id, c.expression, c.timezone, c.input));
637
+ const nextCronIds = cron.map((c) => this.computeCronId(name, c.id, c.expression, c.timezone, c.input));
693
638
 
694
639
  const nextCronIdSet = new Set(nextCronIds);
695
640
  for (const oldId of prevCronIds) {
@@ -699,23 +644,23 @@ export class RedflowClient {
699
644
  }
700
645
  }
701
646
 
702
- for (let i = 0; i < (triggers.cron ?? []).length; i++) {
703
- const cron = triggers.cron![i]!;
647
+ for (let i = 0; i < cron.length; i++) {
648
+ const c = cron[i]!;
704
649
  const cronId = nextCronIds[i]!;
705
- const cronInput = Object.prototype.hasOwnProperty.call(cron, "input") ? cron.input : {};
650
+ const cronInput = Object.prototype.hasOwnProperty.call(c, "input") ? c.input : {};
706
651
 
707
652
  const cronDef = {
708
653
  id: cronId,
709
654
  workflow: name,
710
655
  queue,
711
- expression: cron.expression,
712
- timezone: cron.timezone,
656
+ expression: c.expression,
657
+ timezone: c.timezone,
713
658
  inputJson: safeJsonStringify(cronInput),
714
659
  };
715
660
 
716
661
  await this.redis.hset(keys.cronDef(this.prefix), { [cronId]: safeJsonStringify(cronDef) });
717
662
 
718
- const nextAt = this.computeNextCronAtMs(cron.expression, cron.timezone);
663
+ const nextAt = this.computeNextCronAtMs(c.expression, c.timezone);
719
664
  if (nextAt != null) {
720
665
  await this.redis.zadd(keys.cronNext(this.prefix), nextAt, cronId);
721
666
  } else {
@@ -728,9 +673,8 @@ export class RedflowClient {
728
673
  queue,
729
674
  owner,
730
675
  updatedAt: String(updatedAt),
731
- triggersJson: safeJsonStringify(triggers),
676
+ cronJson: safeJsonStringify(cron),
732
677
  retriesJson: safeJsonStringify(retries),
733
- eventsJson: safeJsonStringify(nextEvents),
734
678
  cronIdsJson: safeJsonStringify(nextCronIds),
735
679
  };
736
680
 
@@ -803,11 +747,6 @@ export class RedflowClient {
803
747
  private async deleteWorkflowMetadata(workflowName: string): Promise<void> {
804
748
  const workflowKey = keys.workflow(this.prefix, workflowName);
805
749
 
806
- const prevEvents = safeJsonTryParse<string[]>(await this.redis.hget(workflowKey, "eventsJson")) ?? [];
807
- for (const eventName of prevEvents) {
808
- await this.redis.srem(keys.eventWorkflows(this.prefix, eventName), workflowName);
809
- }
810
-
811
750
  const prevCronIds = safeJsonTryParse<string[]>(await this.redis.hget(workflowKey, "cronIdsJson")) ?? [];
812
751
  for (const cronId of prevCronIds) {
813
752
  await this.redis.hdel(keys.cronDef(this.prefix), cronId);
@@ -1108,4 +1047,4 @@ export function makeErrorJson(err: unknown): string {
1108
1047
  } catch {
1109
1048
  return '{"name":"Error","message":"Failed to serialize original error","kind":"error"}';
1110
1049
  }
1111
- }
1050
+ }
@@ -16,7 +16,6 @@ export const keys = {
16
16
  // Keep run indexes in a separate namespace to avoid collisions with workflow
17
17
  // metadata keys when workflow names contain suffix-like segments (e.g. ":runs").
18
18
  workflowRuns: (prefix: KeyPrefix, workflowName: string) => withPrefix(prefix, `workflow-runs:${workflowName}`),
19
- eventWorkflows: (prefix: KeyPrefix, eventName: string) => withPrefix(prefix, `event:${eventName}:workflows`),
20
19
 
21
20
  runsCreated: (prefix: KeyPrefix) => withPrefix(prefix, "runs:created"),
22
21
  runsStatus: (prefix: KeyPrefix, status: string) => withPrefix(prefix, `runs:status:${status}`),
@@ -35,4 +34,4 @@ export const keys = {
35
34
 
36
35
  idempotency: (prefix: KeyPrefix, workflowName: string, idempotencyKey: string) =>
37
36
  withPrefix(prefix, `idempo:${encodeCompositePart(workflowName)}:${encodeCompositePart(idempotencyKey)}`),
38
- };
37
+ };
package/src/types.ts CHANGED
@@ -3,11 +3,6 @@ import type { ZodTypeAny } from "zod";
3
3
  export type RunStatus = "scheduled" | "queued" | "running" | "succeeded" | "failed" | "canceled";
4
4
  export type StepStatus = "running" | "succeeded" | "failed";
5
5
 
6
- export type WorkflowTriggers = {
7
- events?: string[];
8
- cron?: CronTrigger[];
9
- };
10
-
11
6
  export type CronTrigger = {
12
7
  /** Standard cron expression with seconds support (6-field) via croner. */
13
8
  expression: string;
@@ -38,7 +33,7 @@ export type DefineWorkflowOptions<TSchema extends ZodTypeAny | undefined = ZodTy
38
33
  name: string;
39
34
  queue?: string;
40
35
  schema?: TSchema;
41
- triggers?: WorkflowTriggers;
36
+ cron?: CronTrigger[];
42
37
  retries?: WorkflowRetries;
43
38
  /** Called when the run reaches terminal failure (retries exhausted or non-retriable error). Not called on cancellation. */
44
39
  onFailure?: (ctx: OnFailureContext) => void | Promise<void>;
@@ -46,15 +41,20 @@ export type DefineWorkflowOptions<TSchema extends ZodTypeAny | undefined = ZodTy
46
41
 
47
42
  export type StepRunOptions = {
48
43
  name: string;
44
+ /**
45
+ * Overall timeout for the step execution.
46
+ * For step.runWorkflow this also bounds child result waiting.
47
+ */
49
48
  timeoutMs?: number;
50
49
  };
51
50
 
52
51
  export type StepRunWorkflowOptions = StepRunOptions & {
53
- run?: Omit<RunOptions, "idempotencyKey" | "__maxAttemptsOverride">;
54
- result?: {
55
- timeoutMs?: number;
56
- pollMs?: number;
57
- };
52
+ /** Earliest time when the child run becomes eligible for processing. */
53
+ runAt?: Date;
54
+ /** Override the queue for the child run. */
55
+ queueOverride?: string;
56
+ /** TTL for the idempotency key in seconds. Default: 7 days. */
57
+ idempotencyTtl?: number;
58
58
  /**
59
59
  * Optional override for child-run idempotency.
60
60
  * By default, redflow derives a stable key from parent run id + step name + child workflow name.
@@ -62,28 +62,23 @@ export type StepRunWorkflowOptions = StepRunOptions & {
62
62
  idempotencyKey?: string;
63
63
  };
64
64
 
65
- export type StepEmitEventOptions = StepRunOptions & {
66
- event: string;
67
- emit?: Omit<EmitEventOptions, "idempotencyKey">;
65
+ export type StepEmitWorkflowOptions = StepRunOptions & {
66
+ /** Earliest time when the run becomes eligible for processing. */
67
+ runAt?: Date;
68
+ /** Override the queue for the run. */
69
+ queueOverride?: string;
70
+ /** TTL for the idempotency key in seconds. Default: 7 days. */
71
+ idempotencyTtl?: number;
68
72
  /**
69
73
  * Optional override for emit idempotency.
70
- * By default, redflow derives a stable key from parent run id + step name + event name.
71
- */
72
- idempotencyKey?: string;
73
- };
74
-
75
- export type StepScheduleEventOptions = StepRunOptions & {
76
- event: string;
77
- schedule: Omit<ScheduleEventOptions, "idempotencyKey">;
78
- /**
79
- * Optional override for schedule idempotency.
80
- * By default, redflow derives a stable key from parent run id + step name + event name.
74
+ * By default, redflow derives a stable key from parent run id + step name + workflow name.
81
75
  */
82
76
  idempotencyKey?: string;
83
77
  };
84
78
 
85
79
  export type RunOptions = {
86
- availableAt?: Date;
80
+ /** Earliest time when the run becomes eligible for processing. */
81
+ runAt?: Date;
87
82
  idempotencyKey?: string;
88
83
  /** TTL for the idempotency key in seconds. Default: 7 days (604 800s). */
89
84
  idempotencyTtl?: number;
@@ -92,20 +87,18 @@ export type RunOptions = {
92
87
  __maxAttemptsOverride?: number;
93
88
  };
94
89
 
95
- export type EmitEventOptions = {
90
+ export type EmitWorkflowOptions = {
91
+ /** Earliest time when the run becomes eligible for processing. */
92
+ runAt?: Date;
96
93
  idempotencyKey?: string;
97
94
  /** TTL for the idempotency key in seconds. Default: 7 days (604 800s). */
98
95
  idempotencyTtl?: number;
99
96
  };
100
97
 
101
- export type ScheduleEventOptions = EmitEventOptions & {
102
- availableAt: Date;
103
- };
104
-
105
98
  export type RunHandle<TOutput = unknown> = {
106
99
  id: string;
107
100
  getState(): Promise<RunState | null>;
108
- result(options?: { timeoutMs?: number; pollMs?: number }): Promise<TOutput>;
101
+ result(options?: { timeoutMs?: number }): Promise<TOutput>;
109
102
  };
110
103
 
111
104
  export type WorkflowLike<TInput = unknown, TOutput = unknown> = {
@@ -120,8 +113,7 @@ export type StepApi = {
120
113
  workflow: WorkflowLike<TInput, TOutput>,
121
114
  input: TInput,
122
115
  ): Promise<TOutput>;
123
- emitEvent(options: StepEmitEventOptions, payload: unknown): Promise<string[]>;
124
- scheduleEvent(options: StepScheduleEventOptions, payload: unknown): Promise<string[]>;
116
+ emitWorkflow(options: StepEmitWorkflowOptions, workflow: WorkflowLike, input: unknown): Promise<string>;
125
117
  };
126
118
 
127
119
  export type RunState = {
@@ -175,7 +167,7 @@ export type ListedRun = {
175
167
  export type WorkflowMeta = {
176
168
  name: string;
177
169
  queue: string;
178
- triggers?: WorkflowTriggers;
170
+ cron?: CronTrigger[];
179
171
  retries?: WorkflowRetries;
180
172
  updatedAt: number;
181
- };
173
+ };