The most flexible activity type in the HotMesh YAML DAG. Depending on its configuration it operates as one of four distinct flavors:

Flavor Key field Behavior
Time sleep Pauses for a duration, then resumes
Signal hook.topic Pauses until an external signal arrives
Cycle cycle: true Passthrough that also accepts re-entry from a cycle activity
Passthrough (none) Maps data and transitions immediately

Pauses the flow for sleep seconds. The value can be a literal number or a @pipe expression (e.g., for exponential backoff).

activities:
t1:
type: trigger

wait_30s:
type: hook
sleep: 30 # pause for 30 seconds
job:
maps:
paused_at: '{$self.output.metadata.ac}'

next_step:
type: hook

transitions:
t1:
- to: wait_30s
wait_30s:
- to: next_step

Dynamic delay with @pipe (exponential backoff on retry):

wait_retry:
type: hook
sleep:
'@pipe':
- ['{$self.output.data.attempt}', 2]
- ['{@math.pow}'] # 1, 2, 4, 8, 16 …

Registers a listener on a named topic. The flow pauses until an external caller delivers a signal. Signal data is available as $self.hook.data. The graph-level hooks section routes incoming signals to the waiting activity via a conditions.match rule.

Send the signal from any process:

await hotMesh.signal('order.approval', { id: jobId, approved: true });

Claim and delete a pending signal via the collator key:

// Ack/delete — deliver a signal and clear the hook registration
await hotMesh.signal('order.approval', { id: jobId, approved: false });

YAML configuration:

app:
id: myapp
version: '1'
graphs:
- subscribes: order.placed
expire: 3600

activities:
t1:
type: trigger

wait_for_approval:
type: hook
hook:
type: object
properties:
approved: { type: boolean }
job:
maps:
approved: '{$self.hook.data.approved}'

done:
type: hook

transitions:
t1:
- to: wait_for_approval
wait_for_approval:
- to: done

hooks:
order.approval: # topic delivering the signal
- to: wait_for_approval
conditions:
match:
- expected: '{t1.output.data.id}' # job ID
actual: '{$self.hook.data.id}' # signal payload ID

Adding an escalation: block causes the hook activity to write one row to public.hmsh_escalations atomically inside its Leg 1 transaction — the same database commit that checkpoints job state. The row is immediately queryable and claimable by any external system.

All field values support @pipe expressions so they can reference job data computed by earlier activities (e.g., '{t1.output.data.region}').

wait_for_approval:
type: hook
hook:
type: object
properties:
approved: { type: boolean }
escalation:
role: manager # RBAC role that should act
type: order-approval
subtype: regional
priority: 2 # lower = higher priority
description: Approve or reject the order
entity: '{t1.output.data.entityType}'
metadata:
orderId: '{t1.output.data.orderId}'
region: '{t1.output.data.region}'
envelope:
instructions: Review the attached order and approve or reject
expiresAt: '{t1.output.data.dueDate}'
job:
maps:
approved: '{$self.hook.data.approved}'

Claim and resolve the escalation (resumes the waiting workflow):

// Find pending approvals for the manager role
const [item] = await client.escalations.list({ role: 'manager', status: 'pending' });

// Claim it (sets assigned_to + claim_expires_at)
const claim = await client.escalations.claim({
id: item.id,
assignee: 'alice@company.com',
durationMinutes: 30,
});

// Resolve atomically delivers the signal and resumes the workflow
await client.escalations.resolve({
id: item.id,
resolverPayload: { approved: true },
});

A passthrough hook with cycle: true acts as the named re-entry point for a cycle activity. On first entry it behaves identically to a passthrough (maps data, transitions forward). When a cycle activity downstream names it as its ancestor, the engine routes execution back to it, allowing a controlled loop without spawning a new job.

app:
id: myapp
version: '1'
graphs:
- subscribes: job.start
expire: 120

activities:
t1:
type: trigger

pivot:
type: hook
cycle: true # marks this as a loop re-entry point

do_work:
type: worker
topic: work.process
output:
schema:
type: object
properties:
counter: { type: number }
job:
maps:
counter: '{$self.output.data.counter}'

loop_back:
type: cycle
ancestor: pivot # jumps back to `pivot` when condition holds

transitions:
t1:
- to: pivot
pivot:
- to: do_work
do_work:
- to: loop_back
conditions:
match:
- expected: true
actual:
'@pipe':
- ['{do_work.output.data.counter}', 5]
- ['{@conditional.less_than}']

When none of sleep, hook, or cycle is set, the hook activity immediately maps data and transitions to its children. Useful as a data transformation node or fan-in convergence point.

merge:
type: hook
output:
maps:
total: '{a1.output.data.subtotal}' # copy field into activity output
job:
maps:
total: '{$self.output.data.total}' # promote to job-level data

  • Time and Signal flavors — Category A (duplex). Leg 1 registers the hook (timer or webhook), saves state, and commits. Leg 2 fires when the timer fires or the external signal arrives.
  • Cycle and Passthrough flavors — Category B. Uses the crash-safe executeLeg1StepProtocol (GUID ledger backed) to map data and immediately transition to adjacent activities.

HookActivity for the TypeScript interface

Hierarchy (view full)

Constructors

Properties

adjacencyList: StreamData[]
adjacentIndex: number = 0
code: number = 200
config: HookActivity
context: JobState
guidLedger: number = 0
logger: ILogger
status: StreamStatus = StreamStatus.SUCCESS

Methods

  • Parameters

    Returns Promise<void | {
        jobId?: string;
        pending?: string;
    }>

    Use registerTimeHook + registerWebHookSignal instead. Kept for backward compatibility with tests that monkey-patch this method.

  • Register the time hook (sleep) inside the Leg1 transaction. Time hooks don't participate in the signal race — they're purely internal timeout registrations.

    Parameters

    Returns Promise<void>

  • Register the web hook signal AFTER the Leg1 transaction commits. This ensures the hook signal is never visible before Leg1 completion, eliminating the FORBIDDEN window where Leg2 could find the hook but fail on the collation check.

    If a pending signal was stored by an early-arriving Leg2, setHookSignal atomically detects and returns it.

    Returns Promise<void | {
        pending?: string;
    }>