agentsclimarketplace

Resonate durable sleep scheduled work typescript

Skill resonatehq/resonate-skills/resonate-durable-sleep-scheduled-work-typescript

Agent skills for building with Resonate — durable execution for long-running, crash-safe workflows.

Install
npx -y skills add resonatehq/resonate-skills --skill resonate-durable-sleep-scheduled-work-typescript

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 5 stars5 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

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.

The file declares its own license as Apache-2.0. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

12.9 KB, as published. Nobody here has run it

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 <= totalOccurrences; i++) {
    // Send notification
    yield* ctx.run(async () => {
      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 < totalOccurrences) {
      yield* ctx.sleep(intervalHours * 60 * 60 * 1000);
    }
  }

  return { completed: true, sent: totalOccurrences };
}

// Send daily notifications for 7 days
await resonate.run(
  `daily-updates/${userId}`,
  recurringNotification,
  "[email protected]",
  24,  // every 24 hours
  7    // 7 times total
);

Pattern: Exponential Backoff with Sleep

function* retryWithBackoff(
  ctx: Context,
  operation: string,
  maxAttempts: number = 5
) {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      // Try the operation
      const result = yield* ctx.run(performOperation, operation);
      return { success: true, attempt, result };
    } catch (error) {
      if (attempt === maxAttempts) {
        // Final attempt failed
        return { success: false, attempts: maxAttempts, error };
      }

      // Exponential backoff: 2^attempt seconds
      const delayMs = Math.pow(2, attempt) * 1000;
      console.log(`Attempt ${attempt} failed, retrying in ${delayMs}ms`);

      // Durable sleep before retry
      yield* ctx.sleep(delayMs);
    }
  }
}

Pattern: Rate Limiting with Sleep

function* processWithRateLimit(
  ctx: Context,
  items: string[],
  itemsPerMinute: number
) {
  const delayMs = (60 * 1000) / itemsPerMinute;
  const results = [];

  for (let i = 0; i < items.length; i++) {
    // Process item
    const result = yield* ctx.run(processItem, items[i]);
    results.push(result);

    // Sleep to enforce rate limit (except after last item)
    if (i < items.length - 1) {
      yield* ctx.sleep(delayMs);
    }
  }

  return { processed: results.length, results };
}

// Process 10 items per minute
await resonate.run(
  "batch-process-1",
  processWithRateLimit,
  items,
  10  // 10 items per minute
);

Pattern: Multi-Stage Delayed Workflow

function* onboardingFlow(
  ctx: Context,
  userId: string,
  email: string
) {
  // Day 0: Welcome email
  yield* ctx.run(sendEmail, email, "Welcome!", welcomeTemplate);

  // Day 1: Sleep 24 hours, send tips
  yield* ctx.sleep(24 * 60 * 60 * 1000);
  yield* ctx.run(sendEmail, email, "Getting Started Tips", tipsTemplate);

  // Day 3: Sleep 48 hours, send feature highlight
  yield* ctx.sleep(48 * 60 * 60 * 1000);
  yield* ctx.run(sendEmail, email, "Feature Highlight", featureTemplate);

  // Day 7: Sleep 96 hours, send survey
  yield* ctx.sleep(96 * 60 * 60 * 1000);
  yield* ctx.run(sendEmail, email, "How are we doing?", surveyTemplate);

  return { completed: true, userId };
}

Pattern: Scheduled Cleanup Job

function* scheduledCleanup(
  ctx: Context,
  intervalDays: number
) {
  while (true) {
    // Perform cleanup
    const deleted = yield* ctx.run(cleanupOldRecords);

    console.log(`Deleted ${deleted} records`);

    // Sleep until next cleanup
    yield* ctx.sleep(intervalDays * 24 * 60 * 60 * 1000);
  }
}

async function cleanupOldRecords(_ctx: Context): Promise<number> {
  // 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

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.