AgentStack
SKILL verified Apache-2.0 Self-run

Resonate Durable Sleep Scheduled Work Typescript

skill-resonatehq-resonate-skills-resonate-durable-sleep-scheduled-work-typescript · by resonatehq

Implement durable sleep and scheduled work patterns for long-running timers, countdowns, scheduled notifications, and delayed execution. Use ctx.sleep() for delays that survive crashes and span hours, days, or weeks.

No reviews yet
0 installs
9 views
0.0% view→install

Install

$ agentstack add skill-resonatehq-resonate-skills-resonate-durable-sleep-scheduled-work-typescript

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access Used
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Resonate Durable Sleep Scheduled Work Typescript? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

About

Resonate Durable Sleep & Scheduled Work (TypeScript)

Overview

Resonate's ctx.sleep() creates suspension points where workflows pause without consuming resources. Unlike regular setTimeout() or sleep(), durable sleep survives crashes, restarts, and redeployments. The workflow suspends its state, and Resonate resumes it exactly where it left off after the delay expires.

Core principle: yield* ctx.sleep(milliseconds) suspends execution durably. Workflows can sleep for seconds, hours, days, or weeks without holding threads, connections, or memory.

Mental Model

Regular Sleep (Ephemeral):
  Process → sleep(10h) → [BLOCKS THREAD] → Resume
  Crash during sleep? → LOST, never resumes

Durable Sleep (Resonate):
  Workflow → yield* ctx.sleep(10h) → [SUSPENDS STATE] → Resume
  Crash during sleep? → Automatically resumes after delay
  Restart process? → Resumes from checkpoint after delay

No resources held during sleep - workflow exists only as durable state.

Basic Pattern: Simple Countdown

import { Context } from "@resonatehq/sdk";

function* countdown(
  ctx: Context,
  count: number,
  delayMinutes: number,
  notificationUrl: string
) {
  for (let i = count; i > 0; i--) {
    // Send notification
    yield* ctx.run(sendNotification, notificationUrl, `Countdown: ${i}`);

    // Durable sleep - workflow suspends here
    yield* ctx.sleep(delayMinutes * 60 * 1000);
  }

  // Send final notification
  yield* ctx.run(sendNotification, notificationUrl, "Done!");
}

async function sendNotification(
  _ctx: Context,
  url: string,
  message: string
) {
  await fetch(url, {
    method: "POST",
    body: message,
    headers: { "Content-Type": "text/plain" }
  });
}

Usage:

// Count down from 5, waiting 1 minute between each count
await resonate.run(
  "countdown-1",
  countdown,
  5,              // count
  1,              // delay in minutes
  "https://ntfy.sh/mychannel"
);

What happens:

  1. Sends "Countdown: 5"
  2. Sleeps 1 minute (workflow suspends, no resources held)
  3. Resumes, sends "Countdown: 4"
  4. Sleeps 1 minute
  5. Repeats until "Done!"

Pattern: Scheduled Reminder

function* scheduleReminder(
  ctx: Context,
  userId: string,
  message: string,
  delayMs: number
) {
  // Sleep until reminder time
  yield* ctx.sleep(delayMs);

  // Send reminder
  yield* ctx.run(sendEmail, userId, "Reminder", message);

  return { sent: true, timestamp: Date.now() };
}

// Schedule reminder 24 hours from now
await resonate.run(
  `reminder/${userId}/${Date.now()}`,
  scheduleReminder,
  "user-123",
  "Don't forget to review the document!",
  24 * 60 * 60 * 1000  // 24 hours
);

Pattern: Recurring Notification

function* recurringNotification(
  ctx: Context,
  userId: string,
  intervalHours: number,
  totalOccurrences: number
) {
  for (let i = 1; i  {
      await emailService.send({
        to: userId,
        subject: `Daily Update #${i}`,
        body: `This is occurrence ${i} of ${totalOccurrences}`
      });
    });

    // Sleep until next occurrence (unless this was the last one)
    if (i  {
  // Delete records older than 90 days
  const cutoffDate = new Date();
  cutoffDate.setDate(cutoffDate.getDate() - 90);

  const result = await db.delete({
    table: "logs",
    where: { created_at: { lt: cutoffDate } }
  });

  return result.deletedCount;
}

// Run cleanup every 7 days
await resonate.run(
  "cleanup-job",
  scheduledCleanup,
  7  // every 7 days
);

Pattern: Timeout with Sleep

function* operationWithTimeout(
  ctx: Context,
  taskId: string,
  timeoutMinutes: number
) {
  // Start the operation
  const handle = yield* ctx.beginRpc(longRunningOperation, taskId);

  // Start a timeout timer
  const timeoutHandle = yield* ctx.beginRpc(
    timeoutTimer,
    timeoutMinutes
  );

  // Race between operation and timeout
  // Note: Resonate doesn't have built-in race(), so we check completion
  try {
    const result = yield* handle;
    return { success: true, result };
  } catch (error) {
    return { success: false, timedOut: true };
  }
}

function* timeoutTimer(ctx: Context, minutes: number) {
  yield* ctx.sleep(minutes * 60 * 1000);
  throw new Error("Operation timed out");
}

Pattern: Scheduled Batch Processing

function* batchProcessingScheduler(
  ctx: Context,
  batchSize: number,
  intervalHours: number
) {
  while (true) {
    // Fetch batch of items to process
    const items = yield* ctx.run(fetchPendingItems, batchSize);

    if (items.length === 0) {
      console.log("No items to process");
    } else {
      // Process each item
      for (const item of items) {
        yield* ctx.run(processItem, item);
      }

      console.log(`Processed ${items.length} items`);
    }

    // Sleep until next batch
    yield* ctx.sleep(intervalHours * 60 * 60 * 1000);
  }
}

// Process batches of 100 items every 6 hours
await resonate.run(
  "batch-processor",
  batchProcessingScheduler,
  100,  // batch size
  6     // every 6 hours
);

Real-World Example: Trial Expiration Workflow

function* trialExpirationWorkflow(
  ctx: Context,
  userId: string,
  trialDays: number
) {
  const trialEndDate = new Date();
  trialEndDate.setDate(trialEndDate.getDate() + trialDays);

  // Send welcome email immediately
  yield* ctx.run(sendEmail, userId, "Trial Started", welcomeEmail);

  // Day 7: Reminder
  yield* ctx.sleep(7 * 24 * 60 * 60 * 1000);
  yield* ctx.run(sendEmail, userId, "Trial Reminder", reminderEmail);

  // Day 13: Upgrade prompt
  yield* ctx.sleep(6 * 24 * 60 * 60 * 1000);
  yield* ctx.run(sendEmail, userId, "Upgrade Now", upgradeEmail);

  // Day 14: Trial expires
  yield* ctx.sleep(1 * 24 * 60 * 60 * 1000);

  // Check if user upgraded
  const user = yield* ctx.run(getUser, userId);

  if (!user.isPaid) {
    // Downgrade to free tier
    yield* ctx.run(downgradeAccount, userId);
    yield* ctx.run(sendEmail, userId, "Trial Ended", trialEndedEmail);
  }

  return { userId, upgraded: user.isPaid };
}

Crash Recovery Behavior

Scenario: Countdown crashes during sleep

Initial execution:
  ✓ Send "Countdown: 5"
  ✓ ctx.sleep(60000) - Creates durable timer
  ✗ CRASH

Resume (after worker restarts):
  → Resonate replays from beginning
  → Send "Countdown: 5" - Idempotent (or checkpointed)
  → ctx.sleep(60000) - Resonate knows this already completed, skips
  → After sleep expires, workflow automatically resumes
  → Continues with "Countdown: 4"

Key insight: Crashes during sleep don't lose the timer. Resonate tracks the sleep's expiration time durably.

Sleep Duration Limits

Practical limits:

  • Milliseconds: Minimum sleep duration
  • Days/Weeks: Common for scheduled workflows
  • Months: Possible but consider alternatives for very long delays
  • Maximum: No hard limit, but long sleeps may require server configuration

Recommendation: For delays > 90 days, consider using a cron-style scheduler or external job queue.

Common Pitfalls

1. Using setTimeout Instead of ctx.sleep

// ❌ WRONG - Not durable, lost on crash
function* badCountdown(ctx: Context, count: number) {
  for (let i = count; i > 0; i--) {
    yield* ctx.run(notify, `Count: ${i}`);
    await new Promise(resolve => setTimeout(resolve, 60000));  // LOST ON CRASH
  }
}

// ✅ CORRECT - Durable sleep
function* goodCountdown(ctx: Context, count: number) {
  for (let i = count; i > 0; i--) {
    yield* ctx.run(notify, `Count: ${i}`);
    yield* ctx.sleep(60000);  // SURVIVES CRASHES
  }
}

2. Sleeping Outside Generator Context

// ❌ WRONG - Can't sleep outside ctx
async function badDelay(ctx: Context) {
  await ctx.run(async () => {
    await new Promise(resolve => setTimeout(resolve, 60000));  // NOT DURABLE
  });
}

// ✅ CORRECT - Sleep in generator
function* goodDelay(ctx: Context) {
  yield* ctx.sleep(60000);  // DURABLE
}

3. Not Handling Idempotency

// ❌ WRONG - Sends duplicate notifications on retry
function* badReminder(ctx: Context, userId: string) {
  await sendEmail(userId, "Reminder");  // NOT IDEMPOTENT
  yield* ctx.sleep(24 * 60 * 60 * 1000);
}

// ✅ CORRECT - Wrapped in ctx.run for idempotency
function* goodReminder(ctx: Context, userId: string) {
  yield* ctx.run(sendEmail, userId, "Reminder");  // IDEMPOTENT
  yield* ctx.sleep(24 * 60 * 60 * 1000);
}

Time Precision

Resonate's sleep precision:

  • Sleep duration specified in milliseconds
  • Actual resume time may vary by seconds (depends on server load, polling intervals)
  • Not suitable for sub-second precision requirements
  • Suitable for minutes, hours, days

Example:

yield* ctx.sleep(60000);  // Sleeps ~60 seconds (±few seconds variance)
yield* ctx.sleep(100);    // Not reliable for 100ms precision

Decision Tree

Use ctx.sleep() when:

  • Delays span minutes, hours, or days
  • Workflow must survive crashes during delay
  • Scheduled notifications, reminders, timeouts
  • Rate limiting, backoff, periodic tasks

Don't use ctx.sleep() when:

  • Sub-second precision required (use regular timers)
  • Delay is ephemeral (use setTimeout in ctx.run)
  • Need to cancel/update sleep dynamically (use promises with timeout)

Summary

Durable sleep enables workflows to:

  • Pause execution for hours, days, or weeks without consuming resources
  • Survive crashes, restarts, and redeployments during sleep
  • Implement countdowns, scheduled reminders, recurring tasks
  • Handle timeouts, rate limiting, exponential backoff
  • Create multi-stage delayed workflows

Core recipe: yield* ctx.sleep(milliseconds) → Workflow suspends → Resonate resumes after delay → Continue execution

Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

Install and usage instructions live in the source repository linked above.

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.