• Pauses the workflow until a signal with the given signalId is received. The workflow suspends durably — it survives process restarts and will resume exactly once when the matching signal() call delivers data.

    condition is the receive side of the signal coordination pair. The send side is signal(), which can be called from another workflow, a hook function, or externally via client.workflow.signal().

    On replay, condition returns the previously stored signal payload immediately — no actual suspension occurs.

    import { Durable } from '@hotmeshio/hotmesh';

    export async function approvalWorkflow(orderId: string): Promise<boolean> {
    const { submitForReview } = Durable.workflow.proxyActivities<typeof activities>();
    await submitForReview(orderId);

    // Pause until a human approves or rejects
    const decision = await Durable.workflow.condition<{ approved: boolean }>('approval');
    return decision.approved;
    }

    // From an API handler or another workflow:
    await client.workflow.signal('approval', { approved: true });

    Pass a duration string as the second argument to set a deadline. condition returns false if the timeout fires before a signal arrives.

    const decision = await Durable.workflow.condition<{ approved: boolean }>(
    'approval',
    '72h', // give reviewers 72 hours; returns false on timeout
    );
    if (decision === false) return 'auto-rejected-timeout';
    return decision.approved ? 'approved' : 'rejected';

    Pass a ConditionQueueConfig as the second argument to surface the pause as a claimable row in public.hmsh_escalations. The INSERT — every field of the config, including metadata facets — is committed atomically with the workflow checkpoint: one write, one commit, crash-safe. From the row's first visible moment it carries its complete routing context and metadata, so claim-by-metadata routing and version-pinned facets (e.g. a schema_version the resolver UI renders) can trust every row they read.

    const decision = await Durable.workflow.condition<{ approved: boolean }>(
    'manager-approval',
    {
    role: 'manager',
    type: 'order-approval',
    subtype: 'regional',
    priority: 2,
    description: 'Approve or reject the regional order',
    metadata: { orderId, region },
    envelope: { instructions: 'Review the attached order' },
    timeout: '72h', // SLA: resume with false + expire the row if unresolved
    },
    );
    if (decision === false) return 'auto-rejected-sla'; // row is now status='expired'

    // Elsewhere: list, claim, then resolve (resumes the workflow)
    const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });
    await client.escalations.claim({ id: item.id, assignee: 'alice@company.com' });
    await client.escalations.resolve({ id: item.id, resolverPayload: { approved: true } });

    Set assignee in the config to write assigned_to in the same atomic commit — the row is born routed to that user (e.g. hand the follow-on step to $resolution.resolvedBy of a prior escalation) and is resolvable by them immediately. Add durationMinutes to arm the claim TTL window at creation, locking the row to the assignee exactly as claim() would.

    Set batch in the config to declare the wait as an accumulator. Each declared item key is filled exactly once via client.escalations.resolveBatchItem(); interim fills are cheap row updates (accepted, row stays pending), and the LAST fill resolves the row and resumes the wait with the full collection — one atomic statement. Type the generic as the collection:

    const parts = await Durable.workflow.condition<
    Record<'cut' | 'weld' | 'paint', StationResult>
    >(signalId, {
    role: 'assembly',
    batch: ['cut', 'weld', 'paint'],
    metadata: { orderId },
    timeout: '24h',
    });
    if (parts === false) return 'sla-expired'; // partial items audit on the expired row
    if (parts === null) return 'cancelled';
    parts.weld; // typed item payload

    The row's metadata.batch_pending / batch_count facets track progress (@>-queryable); payloads accumulate in envelope.batch_items. A plain resolve() on a batch row remains an admin override that resolves the whole row with the payload given.

    The resolve/signal delivery pipeline routes to the main flow's waiter. Inside a hook function (execHook), an escalation-bearing wait writes its row and honors timeout (the row expires and the hook resumes with false), while resolution delivery targets the main flow — so structure SLA-gated human waits in the workflow body and let hook functions report back via signal().

    A signal delivered before its condition() registers — a fast signaler, or a payload deposited before the workflow starts — is buffered as a pending signal and delivered when the wait registers. The buffer holds a signal for 10 minutes by default; pass expire to signal() (e.g. '1h', '30d') to hold it longer when signaling early on purpose.

    const [name, score] = await Promise.all([
    Durable.workflow.condition<string>('name-signal'),
    Durable.workflow.condition<number>('score-signal'),
    ]);

    Harvest fan-out scales the same way: open N waits with Promise.all and signal all of them at once. Buffering covers every signal that races ahead of registration, so size fan-out by the pending-signal TTL — how long a racing signal may wait for its condition to register — with no separate bound on the number of concurrent waits.

    const signalId = `result-${Durable.workflow.random()}`;
    await Durable.workflow.hook({
    taskQueue: 'processors',
    workflowName: 'processItem',
    args: [input, signalId],
    });
    return await Durable.workflow.condition<string>(signalId);

    Type Parameters

    • T

      The type of data expected in the signal payload.

    Parameters

    • signalId: string

      A unique signal identifier shared by the sender and receiver.

    • OptionaltimeoutOrConfig: string | ConditionQueueConfig

      Optional timeout string (e.g. '30s', '24h') OR a ConditionQueueConfig that writes one row to public.hmsh_escalations atomically at suspension time. For an escalation-bearing wait with an SLA, set the config's timeout field — the wait arms the same resume timer as the string form, and when the timer wins the escalation row transitions pending → expired so a late resolve fails as already-expired. (expiresAt is display metadata on the row only; it arms nothing.)

    Returns Promise<T | false | null>

    The signal payload, false when a timeout (string form or config.timeout) expired first, or null if the escalation was cancelled via client.escalations.cancel().