
Condition Based Waiting
- 137 installs
- 63 repo stars
- Updated July 18, 2026
- bobmatnyc/claude-mpm-skills
Implement condition-based waits in agent and browser automation so flows pause until UI state, API responses, or files meet criteria instead of fixed sleeps.
About
condition-based-waiting from bobmatnyc/claude-mpm-skills teaches agents to wait until explicit conditions are met—DOM elements, HTTP status, file presence—rather than arbitrary sleeps. It reduces flaky automation in browser, CLI, and multi-step agent workflows during implementation.
- Predicate-based wait loops
- Avoids brittle fixed delays
- Handles async UI and APIs
- Improves agent reliability
- Patterns for timeout and retry
Condition Based Waiting by the numbers
- 137 all-time installs (skills.sh)
- Ranked #680 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Aug 1, 2026 (Skillselion catalog sync)
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill condition-based-waitingAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 137 |
|---|---|
| repo stars | ★ 63 |
| Last updated | July 18, 2026 |
| Repository | bobmatnyc/claude-mpm-skills ↗ |
What it does
Implement condition-based waits in agent and browser automation so flows pause until UI state, API responses, or files meet criteria instead of fixed sleeps.
Files
Condition-Based Waiting
Overview
Flaky tests often guess at timing with arbitrary delays. This creates race conditions where tests pass on fast machines but fail under load or in CI.
Core principle: Wait for the actual condition you care about, not a guess about how long it takes.
When to Use
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}Use when:
- Tests have arbitrary delays (
setTimeout,sleep,time.sleep()) - Tests are flaky (pass sometimes, fail under load)
- Tests timeout when run in parallel
- Waiting for async operations to complete
Don't use when:
- Testing actual timing behavior (debounce, throttle intervals)
- Always document WHY if using arbitrary timeout
Core Pattern
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();Quick Patterns
| Scenario | Pattern |
|---|---|
| Wait for event | waitFor(() => events.find(e => e.type === 'DONE')) |
| Wait for state | waitFor(() => machine.state === 'ready') |
| Wait for count | waitFor(() => items.length >= 5) |
| Wait for file | waitFor(() => fs.existsSync(path)) |
| Complex condition | waitFor(() => obj.ready && obj.value > 10) |
Implementation
Generic polling function:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}See @example.ts for complete implementation with domain-specific helpers (waitForEvent, waitForEventCount, waitForEventMatch).
For detailed patterns, implementation guide, and common mistakes, see @references/patterns-and-implementation.md
Real-World Impact
From debugging session (2025-10-03):
- Fixed 15 flaky tests across 3 files
- Pass rate: 60% → 100%
- Execution time: 40% faster
- No more race conditions
{
"name": "condition-based-waiting",
"version": "1.0.0",
"category": "universal",
"toolchain": null,
"framework": null,
"tags": [
"debugging",
"frontend",
"async",
"testing"
],
"entry_point_tokens": 55,
"full_tokens": 2385,
"author": "bobmatnyc",
"license": "MIT",
"requires": [],
"updated": "2025-11-21",
"source_path": "testing/condition-based-waiting/SKILL.md",
"source": "https://github.com/bobmatnyc/claude-mpm",
"created": "2025-11-21",
"modified": "2025-11-21",
"maintainer": "Claude MPM Team",
"attribution_required": true,
"repository": "https://github.com/bobmatnyc/claude-mpm-skills"
}
Detailed Patterns and Implementation Guide
This reference provides detailed implementation patterns, common mistakes, and edge cases for condition-based waiting.
Common Mistakes
❌ Polling too fast
// BAD: Wastes CPU
await new Promise(r => setTimeout(r, 1));✅ Fix: Poll every 10ms
await new Promise(r => setTimeout(r, 10)); // Balanced interval❌ No timeout
// BAD: Loop forever if condition never met
while (true) {
const result = condition();
if (result) return result;
await new Promise(r => setTimeout(r, 10));
}✅ Fix: Always include timeout with clear error
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10));
}❌ Stale data
// BAD: Cache state before loop
const state = machine.getState();
await waitFor(() => state === 'ready'); // state never updates!✅ Fix: Call getter inside loop for fresh data
await waitFor(() => machine.getState() === 'ready'); // Fresh on each pollWhen Arbitrary Timeout IS Correct
There are legitimate cases where a fixed timeout is the right approach:
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justifiedRequirements for justified arbitrary timeouts: 1. First wait for triggering condition (condition-based wait comes first) 2. Based on known timing (not guessing - actual system tick rate) 3. Comment explaining WHY timeout is necessary
Advanced Patterns
Waiting with transformation
// Wait for event and return transformed data
const userId = await waitFor(
() => events.find(e => e.type === 'USER_CREATED')?.data.id,
'user creation event'
);Waiting with complex conditions
// Multiple conditions must be met
await waitFor(
() => {
const user = getUser();
return user?.verified && user?.credits > 0 ? user : undefined;
},
'verified user with credits'
);Waiting with side effects
// Log attempts while waiting
let attempts = 0;
await waitFor(
() => {
attempts++;
if (attempts % 10 === 0) {
console.log(`Still waiting after ${attempts} attempts...`);
}
return isReady() || undefined;
},
'system ready'
);Waiting with custom timeouts per condition
// Different timeouts for different scenarios
async function waitForDeploy(environment: string) {
const timeout = environment === 'prod' ? 30000 : 5000;
return waitFor(
() => checkDeployStatus(environment),
`${environment} deployment`,
timeout
);
}Domain-Specific Helpers
When you have common waiting scenarios, create domain-specific helpers:
// Event-based waiting
async function waitForEvent(
manager: EventManager,
eventType: string,
timeoutMs = 5000
) {
return waitFor(
() => manager.getEvents().find(e => e.type === eventType),
`event ${eventType}`,
timeoutMs
);
}
// Count-based waiting
async function waitForEventCount(
manager: EventManager,
minCount: number,
timeoutMs = 5000
) {
return waitFor(
() => {
const events = manager.getEvents();
return events.length >= minCount ? events : undefined;
},
`at least ${minCount} events`,
timeoutMs
);
}
// Pattern matching waiting
async function waitForEventMatch(
manager: EventManager,
matcher: (event: Event) => boolean,
timeoutMs = 5000
) {
return waitFor(
() => manager.getEvents().find(matcher),
'event matching predicate',
timeoutMs
);
}See @example.ts for complete working implementations from real debugging session.
Debugging Tips
Add descriptive error messages
// GOOD: Clear what failed
await waitFor(
() => orders.find(o => o.status === 'SHIPPED'),
'order to be shipped',
5000
);
// Error: "Timeout waiting for order to be shipped after 5000ms"
// BETTER: Include context
await waitFor(
() => orders.find(o => o.id === orderId && o.status === 'SHIPPED'),
`order ${orderId} to be shipped`,
5000
);
// Error: "Timeout waiting for order abc-123 to be shipped after 5000ms"Log current state on timeout
async function waitForWithDebug<T>(
condition: () => T | undefined | null | false,
description: string,
getCurrentState: () => any,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
const state = getCurrentState();
throw new Error(
`Timeout waiting for ${description} after ${timeoutMs}ms. ` +
`Current state: ${JSON.stringify(state)}`
);
}
await new Promise(r => setTimeout(r, 10));
}
}Performance Considerations
Poll interval trade-offs
- 1ms: Too fast, wastes CPU (10,000 checks/second)
- 10ms: Good default, responsive (100 checks/second)
- 50ms: Acceptable for slow operations (20 checks/second)
- 100ms+: Only for very slow operations or known timing
Choose appropriate timeout values
// Fast operations - short timeout
await waitFor(() => cache.get(key), 'cache hit', 1000);
// Network operations - medium timeout
await waitFor(() => fetchStatus(), 'API response', 5000);
// External systems - long timeout
await waitFor(() => checkDeployment(), 'deployment complete', 30000);Avoid expensive condition checks
// BAD: Expensive regex on every poll
await waitFor(() => /complex.*regex.*pattern/.test(getLargeString()), ...);
// GOOD: Cache expensive computations
let parsed;
await waitFor(() => {
const str = getString();
parsed = parsed || expensiveParse(str);
return parsed.isReady;
}, ...);