Loading...
Loading...
Maravilla Cloud durable workflows — replay-based, multi-step processes that survive restarts. Use whenever you need sleeps spanning minutes/hours/days, multi-step pipelines where each step's output feeds the next, waiting for external events, or strict step-history audit. `defineWorkflow` from `@maravilla-labs/functions/workflows/runtime` with `step.run`, `step.sleep`, `step.sleepUntil`, `step.waitForEvent`, `step.invoke`.
npx skill4agent add maravilla-labs/maravilla-cli maravilla-workflowsstep.runstep.sleep('1h')step.sleepUntil(date)step.waitForEventstep.invokeplatform.workflows.start(...)workflows/*.tsdefineWorkflowimport { defineWorkflow } from '@maravilla-labs/functions/workflows/runtime';
interface Input { userId: string; reportId: string; }
export const buildReport = defineWorkflow<Input>(
{ id: 'build-report', options: { retries: 3, timeoutSecs: 60 * 60 } },
async (input, step, ctx) => {
const data = await step.run('fetch-data', async () => {
return await fetchExpensiveData(input.userId);
});
const rendered = await step.run('render', async () => {
return await renderPdf(data);
});
await step.run('upload', async () => {
await ctx.platform.env.STORAGE.put(`reports/${input.reportId}.pdf`, rendered);
});
return { ok: true };
},
);Import note for v0.2.5:is exposed atdefineWorkflow. The docs'@maravilla-labs/functions/workflows/runtimesubpath does not resolve in this release.platform/workflows
/**
* One durable workflow per invitee.
*
* t=0 start (snapshot + go to sleep)
* t=first-grace check 1 — if not clicked, flag `unclicked_first` (amber chip)
* t=first-grace + second check 2 — if still not clicked, flag `unclicked_final` (red chip)
*
* Each `ctx.kv.put` on `inv:{nanoid}` fires a REN event the owner's guest
* list is already subscribed to, so chips appear live with no reload.
*/
import { defineWorkflow } from '@maravilla-labs/functions/workflows/runtime';
interface Input { inviteeNanoid: string; inviteId: string; ownerUserId: string; }
export const inviteeClickWatch = defineWorkflow<Input>(
{ id: 'invitee-click-watch', options: { retries: 3, timeoutSecs: 7 * 24 * 3600 } },
async (input, step, ctx) => {
const kv = ctx.kv as { get: any; put: any };
const key = `inv:${input.inviteeNanoid}`;
await step.sleep('first-grace', '60s');
const firstOutcome = await step.run('check-1', async () => {
const raw = await kv.get('invites', key);
if (!raw) return 'removed' as const;
const invitee = JSON.parse(raw);
if (invitee.clicked_at) return 'clicked' as const;
invitee.unclicked_first = true;
await kv.put('invites', key, JSON.stringify(invitee));
return 'unclicked' as const;
});
if (firstOutcome === 'removed') return { outcome: 'invitee-removed' };
await step.sleep('second-grace', '120s');
const secondOutcome = await step.run('check-2', async () => {
const raw = await kv.get('invites', key);
if (!raw) return 'removed' as const;
const invitee = JSON.parse(raw);
if (invitee.clicked_at) return 'clicked' as const;
invitee.unclicked_final = true;
await kv.put('invites', key, JSON.stringify(invitee));
return 'unclicked' as const;
});
if (secondOutcome === 'removed') return { outcome: 'invitee-removed' };
return { outcome: 'done', firstOutcome, secondOutcome };
},
);step.runawait fetch(...)await kv.put(...)step.run('name', ...)stepstep.run(name, fn)const result = await step.run('charge-card', async () => {
return await stripe.charges.create({ amount, source });
});fnfnstep.sleep(name, duration)await step.sleep('cool-down', '30s');
await step.sleep('grace', '24h');<n>s<n>m<n>h<n>dstep.sleepUntil(name, date)await step.sleepUntil('event-start', new Date(invite.event_date));Datestep.waitForEvent(name, filter, options?)const payment = await step.waitForEvent('payment-received', {
type: 'payment.completed',
match: { orderId: input.orderId }, // every key must equal in payload
}, { timeoutMs: 60 * 60 * 1000 });
if (!payment) {
return { outcome: 'payment-timeout' };
}platform.workflows.sendEvent('payment.completed', { orderId, ... })matchnullstep.invoke(name, workflowId, input)const handle = await step.invoke('child', 'send-receipt', { orderId, email });
const result = await handle.result();const platform = getPlatform();
// Start
const handle = await platform.workflows.start('build-report', { userId, reportId });
console.log(handle.runId);
// Get status (poll or surface in admin UI)
const run = await handle.status();
// { runId, workflowId, status: 'queued' | 'running' | 'sleeping' | 'waiting_event' | 'completed' | 'failed' | 'cancelled', ... }
// Step history (debugging)
const steps = await handle.history();
// Wait for completion
const output = await handle.result({ timeoutMs: 5 * 60_000 });
// Cancel
const cancelled = await handle.cancel(); // best-effort; returns true if it transitioned
// Get a handle to an existing run (no start)
const existing = platform.workflows.handle(savedRunId);// Anywhere in your runtime code:
const woken = await platform.workflows.sendEvent('payment.completed', {
orderId: '123',
amount: 4200,
});
// woken: number of runs resolvedeventTypetypematchpayloadconst handle = await platform.workflows.start('invitee-click-watch', {
inviteeNanoid, inviteId, ownerUserId,
});
// Multiple starts → multiple runs; design the workflow to be safe on duplicates
// (e.g. include the nanoid in step names, or check a marker before flagging)const existing = await kv.get('workflow-runs', `click-watch:${inviteeNanoid}`);
if (!existing) {
const handle = await platform.workflows.start(/* ... */);
await kv.put('workflow-runs', `click-watch:${inviteeNanoid}`, handle.runId);
}step.runtry {
const charge = await step.run('charge', () => stripe.charges.create(/* ... */));
await step.run('reserve-inventory', () => inventory.reserve(items));
await step.run('ship', () => shipping.create(/* ... */));
} catch (err) {
await step.run('refund', () => stripe.refunds.create({ charge: charge.id }));
throw err;
}await step.sleep('1h-warning', '1h');
await step.run('send-1h-warning', () => platform.push.send(target, /* ... */));
await step.sleep('5m-warning', '55m');
await step.run('send-5m-warning', () => platform.push.send(target, /* ... */));
await step.sleepUntil('event-time', input.event_date);
await step.run('event-started', () => /* ... */);step.runconsole.logfetchkv.putdb.insertOneMath.random()Date.now()crypto.randomUUID()step.runnameoptions.timeoutSecsstep.waitForEvent