Skip to content

TriggerBuilder

Stable · since 0.10.0

The builder returned by createTrigger<S>() (called with no arguments). Accumulates configuration through chainable methods and finalizes with .handle(handler). The handler sees conditions.<key> as NonNullable<...> for every key passed to .require(...) — no !, no early-return narrowing.

import { createTrigger } from '@triggery/core/builder';
import type { TriggerBuilder } from '@triggery/core';

The chainable createTrigger<S>() form lives in the @triggery/core/builder subpath (v0.10+) so apps that only use the imperative createTrigger({...}) form don’t pay for the ~250 B gz builder machinery in their main bundle. The TriggerBuilder<S, R> type is exported from the main @triggery/core for ergonomic type references.

type TriggerBuilder<S extends TriggerSchema, R extends ConditionKey<S> = never> = {
  id(id: string): TriggerBuilder<S, R>;
  events(events: readonly EventKey<S>[]): TriggerBuilder<S, R>;
  require<K extends ConditionKey<S>>(...keys: readonly K[]): TriggerBuilder<S, R | K>;
  conditions(values: { readonly [K in ConditionKey<S>]?: ConditionMap<S>[K] | null }): TriggerBuilder<S, R>;
  schedule(strategy: SchedulerStrategy): TriggerBuilder<S, R>;
  concurrency(strategy: ConcurrencyStrategy): TriggerBuilder<S, R>;
  scope(scope: string): TriggerBuilder<S, R>;
  handle(handler: TriggerHandler<S, R>): Trigger<S>;
};

The type parameter R carries the union of required condition keys accumulated by .require(...). It’s threaded into TriggerHandler<S, R>, which narrows the handler’s conditions.<key> accordingly.

MethodRequiredDescription
.id(string)yesUnique trigger id within the runtime. Must be a string literal.
.events([...])yesEvent names from S['events'] this trigger reacts to.
.conditions({...})noInitial values for conditions held by this trigger. Same shape as the conditions: field on the imperative config.
.require(...keys)noAdd condition keys to the required set. Narrows conditions.<key> to NonNullable<...> inside .handle(...). Multiple .require(...) calls accumulate.
.schedule(strategy)noOverride the scheduler — 'microtask' (default) or 'sync'.
.concurrency(strategy)noOverride concurrency — 'take-latest' (default), 'take-every', 'take-first', 'queue', 'exhaust', 'sync'.
.scope(string)noSet the scope id (for <TriggerScope id="…"> isolation).
.handle(handler)yesFinalize and register the trigger. Returns Trigger<S>. Throws if .id or .events were not called.

The chained order does not affect behaviour — .handle(...) validates that id and events are set.

import { createTriggerfunction createTrigger<S extends TriggerSchema>(runtime?: Runtime): TriggerBuilder<S>
Returns a chainable `TriggerBuilder<S>`. See module docs for examples.
} from '@triggery/core/builder';
type Schema
type Schema = {
    events: {
        "new-message": {
            author: string;
            text: string;
        };
    };
    conditions: {
        user: {
            id: string;
        };
        settings: {
            sound: boolean;
        };
    };
    actions: {
        showToast: {
            title: string;
        };
        playSound: void;
    };
}
= {
events
events: {
    'new-message': {
        author: string;
        text: string;
    };
}
: { 'new-message': { authorauthor: string: string; texttext: string: string } };
conditions
conditions: {
    user: {
        id: string;
    };
    settings: {
        sound: boolean;
    };
}
: { user
user: {
    id: string;
}
: { idid: string: string }; settings
settings: {
    sound: boolean;
}
: { soundsound: boolean: boolean } };
actions
actions: {
    showToast: {
        title: string;
    };
    playSound: void;
}
: { showToast
showToast: {
    title: string;
}
: { titletitle: string: string }; playSoundplaySound: void: void };
}; const messageTriggerconst messageTrigger: Trigger<Schema> = createTriggercreateTrigger<Schema>(runtime?: Runtime): TriggerBuilder<Schema>
Returns a chainable `TriggerBuilder<S>`. See module docs for examples.
<Schema
type Schema = {
    events: {
        "new-message": {
            author: string;
            text: string;
        };
    };
    conditions: {
        user: {
            id: string;
        };
        settings: {
            sound: boolean;
        };
    };
    actions: {
        showToast: {
            title: string;
        };
        playSound: void;
    };
}
>()
.idfunction id(id: string): TriggerBuilder<Schema, never>
Set the unique trigger id (mandatory before `.handle`).
('message-received')
.eventsfunction events(events: readonly "new-message"[]): TriggerBuilder<Schema, never>
Declare the event keys this trigger listens for (mandatory before `.handle`).
(['new-message'])
.conditions
function conditions(values: {
    readonly user?: {
        id: string;
    } | null | undefined;
    readonly settings?: {
        sound: boolean;
    } | null | undefined;
}): TriggerBuilder<Schema, never>
Declare inline conditions (same shape as the imperative `conditions:` field).
({ user
user?: {
    id: string;
} | null | undefined
: null, settings
settings?: {
    sound: boolean;
} | null | undefined
: null })
.requirerequire<"user" | "settings">(...keys: readonly ("user" | "settings")[]): TriggerBuilder<Schema, "user" | "settings">
Add condition keys to the required set. Calling `.require('a').require('b')` is equivalent to `.require('a', 'b')` — required keys accumulate. The handler's `conditions.<key>` becomes `NonNullable<...>` for every listed key.
('user', 'settings')
.handlefunction handle(handler: TriggerHandler<Schema, "user" | "settings">): Trigger<Schema>
Finalize the trigger with the given handler. The handler sees `conditions.<key>` as `NonNullable<…>` for every key passed to `.require`. Returns the live `Trigger<S>`, already registered with the runtime.
(({ event
event: {
    readonly name: "new-message";
    readonly payload: {
        author: string;
        text: string;
    };
}
, conditions
conditions: ConditionsCtx<{
    user: {
        id: string;
    };
    settings: {
        sound: boolean;
    };
}, "user" | "settings">
, actions
actions: ActionsCtx<{
    showToast: {
        title: string;
    };
    playSound: void;
}>
}) => {
// conditions.user: { id: string } ← narrowed by .require // conditions.settings: { sound: boolean } actions
actions: ActionsCtx<{
    showToast: {
        title: string;
    };
    playSound: void;
}>
.showToast
showToast?: ((payload: {
    title: string;
}) => void) | undefined
?.({ titletitle: string: conditions
conditions: ConditionsCtx<{
    user: {
        id: string;
    };
    settings: {
        sound: boolean;
    };
}, "user" | "settings">
.user
user: {
    id: string;
}
.idid: string });
if (conditions
conditions: ConditionsCtx<{
    user: {
        id: string;
    };
    settings: {
        sound: boolean;
    };
}, "user" | "settings">
.settings
settings: {
    sound: boolean;
}
.soundsound: boolean) actions
actions: ActionsCtx<{
    showToast: {
        title: string;
    };
    playSound: void;
}>
.playSoundplaySound?: (() => void) | undefined?.();
});

conditions.user and conditions.settings are NonNullable<...> inside .handle(...) — no !, no if (!conditions.user) return; guards.

Both forms compile to the same Trigger<S> and use the same internal hot path — pick whichever reads better in your codebase.

// Builder form (v0.10+, narrows required)
const t = createTrigger<S>()
  .id('x').events(['e']).require('user').handle(({ conditions }) => {
    return conditions.user.id; // NonNullable
  });

// Imperative form (v0.9+, no narrowing)
const t = createTrigger<S>({
  id: 'x', events: ['e'], required: ['user'],
  handler({ conditions }) {
    if (!conditions.user) return;       // manual guard
    return conditions.user.id;
  },
});

The ESLint rule prefer-builder-trigger (enabled as warn in the strict preset) flags the imperative form when it carries required: [...].