Skip to content

createTrigger

Stable · since 0.1.0

The only constructor in Triggery. Returns a Trigger<Schema> registered in the given runtime (or the default runtime if omitted). Most apps create one trigger per scenario, one file per trigger.

import { createTrigger } from '@triggery/core';
// Imperative form — v0.1+ — from '@triggery/core'
function createTrigger<S extends TriggerSchema>(
  config: CreateTriggerConfig<S>,
  runtime?: Runtime,
): Trigger<S>;

// Builder form — v0.10+ — from '@triggery/core/builder'
function createTrigger<S extends TriggerSchema>(runtime?: Runtime): TriggerBuilder<S>;

The generic S is the schema — three optional maps for events, conditions and actions. It is never inferred in normal use; always passed explicitly.

Both forms produce the same Trigger<S> object at runtime — the builder is purely a typing convenience that narrows required conditions automatically (no !, no if (!conditions.x) return;). See TriggerBuilder below.

type TriggerSchema = {
  events?:     Record<string, unknown>;  // payload type per event name
  conditions?: Record<string, unknown>;  // value type per condition name
  actions?:    Record<string, unknown>;  // payload type per action name
};

For a void payload, use void (events) or undefined (actions). Names can be any valid string; convention is kebab-case verbs for events, camelCase for conditions and actions.

FieldTypeRequiredDefaultDescription
idstring (literal)yesUnique runtime registry key. Must be a string literal — see no-dynamic-id.
eventsreadonly EventKey<S>[]yesEvent names from S['events'] this trigger reacts to.
conditions{ [K in ConditionKey<S>]?: ConditionMap<S>[K] | null }nov0.10+ Initial values for conditions owned by this trigger. Updated via trigger.setCondition(name, value).
requiredreadonly ConditionKey<S>[]no[]Conditions that must be registered (and non-null/non-undefined) for the handler to run.
schedule'microtask' | 'sync'no'microtask'When to dispatch matching events.
concurrencyConcurrencyStrategyno'take-latest'How async handler runs interact.
scopestringno''Restrict to a <TriggerScope id="…"> subtree.
handlerTriggerHandler<S>yesFunction called when an event matches and required are satisfied.

A Trigger<S> object — a stable identity you can pass to React/Solid/Vue hooks.

MethodDescription
trigger.idSame string you passed in.
trigger.enable()Re-enable a disabled trigger.
trigger.disable()Skip future events (records 'disabled' in inspector).
trigger.isEnabled()Boolean.
trigger.setCondition(name, value)v0.10+ Update an inline condition declared in conditions: config. No-op (DEV warn-once) for keys not in config.
trigger.action(name)v0.10+ Get the typed multi-subscriber channel for an action. Cached per (trigger, name). See ActionChannel.
trigger.inspect()Latest TriggerInspectSnapshot or undefined.
trigger.dispose()Unregister from the runtime. Rare — use in tests.
trigger.namedHooks()Throws on @triggery/core directly — use createNamedHooks from a binding.
import { createTriggerfunction createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
} from '@triggery/core';
const ping
const ping: Trigger<{
    events: {
        tick: number;
    };
}>
= createTrigger
createTrigger<{
    events: {
        tick: number;
    };
}>(config: CreateTriggerConfig<{
    events: {
        tick: number;
    };
}>, runtime?: Runtime): Trigger<{
    events: {
        tick: number;
    };
}>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
<{
events
events: {
    tick: number;
}
: { ticktick: number: number };
}>({ idid: string: 'demo:ping', eventsevents: readonly "tick"[]: ['tick'], handler
handler: TriggerHandler<{
    events: {
        tick: number;
    };
}, never>
({ event
event: {
    readonly name: "tick";
    readonly payload: number;
}
}) {
consolevar console: Console.logConsole.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
('tick', event
event: {
    readonly name: "tick";
    readonly payload: number;
}
.payloadpayload: number);
}, });
import { createTriggerfunction createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
} from '@triggery/core';
type Settings
type Settings = {
    sound: boolean;
    notifications: boolean;
}
= { soundsound: boolean: boolean; notificationsnotifications: boolean: boolean };
type Message
type Message = {
    author: string;
    text: string;
    channelId: string;
}
= { authorauthor: string: string; texttext: string: string; channelIdchannelId: string: string };
const messageTrigger
const messageTrigger: Trigger<{
    events: {
        "new-message": Message;
    };
    conditions: {
        settings: Settings;
        activeChannelId: string | null;
    };
    actions: {
        showToast: {
            title: string;
            body: string;
        };
        playSound: "beep";
    };
}>
= createTrigger
createTrigger<{
    events: {
        "new-message": Message;
    };
    conditions: {
        settings: Settings;
        activeChannelId: string | null;
    };
    actions: {
        showToast: {
            title: string;
            body: string;
        };
        playSound: "beep";
    };
}>(config: CreateTriggerConfig<{
    events: {
        "new-message": Message;
    };
    conditions: {
        settings: Settings;
        activeChannelId: string | null;
    };
    actions: {
        showToast: {
            title: string;
            body: string;
        };
        playSound: "beep";
    };
}>, runtime?: Runtime): Trigger<{
    events: {
        "new-message": Message;
    };
    conditions: {
        settings: Settings;
        activeChannelId: string | null;
    };
    actions: {
        showToast: {
            title: string;
            body: string;
        };
        playSound: "beep";
    };
}>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
<{
events
events: {
    'new-message': Message;
}
: { 'new-message': Message
type Message = {
    author: string;
    text: string;
    channelId: string;
}
};
conditions
conditions: {
    settings: Settings;
    activeChannelId: string | null;
}
: { settingssettings: Settings: Settings
type Settings = {
    sound: boolean;
    notifications: boolean;
}
; activeChannelIdactiveChannelId: string | null: string | null };
actions
actions: {
    showToast: {
        title: string;
        body: string;
    };
    playSound: "beep";
}
: { showToast
showToast: {
    title: string;
    body: string;
}
: { titletitle: string: string; bodybody: string: string }; playSoundplaySound: "beep": 'beep' };
}>({ idid: string: 'message-received', eventsevents: readonly "new-message"[]: ['new-message'], requiredrequired?: readonly ("settings" | "activeChannelId")[] | undefined
Required condition keys. The trigger will not run unless all of them are registered.
: ['settings'],
handler
handler: TriggerHandler<{
    events: {
        "new-message": Message;
    };
    conditions: {
        settings: Settings;
        activeChannelId: string | null;
    };
    actions: {
        showToast: {
            title: string;
            body: string;
        };
        playSound: "beep";
    };
}, never>
({ event
event: {
    readonly name: "new-message";
    readonly payload: Message;
}
, conditions
conditions: ConditionsCtx<{
    settings: Settings;
    activeChannelId: string | null;
}, never>
, actions
actions: ActionsCtx<{
    showToast: {
        title: string;
        body: string;
    };
    playSound: "beep";
}>
, check
check: CheckCtx<{
    settings: Settings;
    activeChannelId: string | null;
}>
}) {
if (event
event: {
    readonly name: "new-message";
    readonly payload: Message;
}
.payloadpayload: Message.channelIdchannelId: string === conditions
conditions: ConditionsCtx<{
    settings: Settings;
    activeChannelId: string | null;
}, never>
.activeChannelIdactiveChannelId?: string | null | undefined) return;
if (!check
check: CheckCtx<{
    settings: Settings;
    activeChannelId: string | null;
}>
.isis<"settings">(key: "settings", predicate: (value: Settings) => boolean): boolean('settings', ss: Settings => ss: Settings.notificationsnotifications: boolean)) return;
actions
actions: ActionsCtx<{
    showToast: {
        title: string;
        body: string;
    };
    playSound: "beep";
}>
.debounce
function debounce(ms: number): ActionsCtx<{
    showToast: {
        title: string;
        body: string;
    };
    playSound: "beep";
}>
(800).playSoundplaySound?: ((payload: "beep") => void) | undefined?.('beep');
actions
actions: ActionsCtx<{
    showToast: {
        title: string;
        body: string;
    };
    playSound: "beep";
}>
.showToast
showToast?: ((payload: {
    title: string;
    body: string;
}) => void) | undefined
?.({
titletitle: string: event
event: {
    readonly name: "new-message";
    readonly payload: Message;
}
.payloadpayload: Message.authorauthor: string,
bodybody: string: event
event: {
    readonly name: "new-message";
    readonly payload: Message;
}
.payloadpayload: Message.texttext: string,
}); }, });
import { createTriggerfunction createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
} from '@triggery/core';
const fetchTrigger
const fetchTrigger: Trigger<{
    events: {
        "fetch-user": string;
    };
    actions: {
        setUser: {
            id: string;
            name: string;
        };
    };
}>
= createTrigger
createTrigger<{
    events: {
        "fetch-user": string;
    };
    actions: {
        setUser: {
            id: string;
            name: string;
        };
    };
}>(config: CreateTriggerConfig<{
    events: {
        "fetch-user": string;
    };
    actions: {
        setUser: {
            id: string;
            name: string;
        };
    };
}>, runtime?: Runtime): Trigger<{
    events: {
        "fetch-user": string;
    };
    actions: {
        setUser: {
            id: string;
            name: string;
        };
    };
}>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
<{
events
events: {
    'fetch-user': string;
}
: { 'fetch-user': string };
actions
actions: {
    setUser: {
        id: string;
        name: string;
    };
}
: { setUser
setUser: {
    id: string;
    name: string;
}
: { idid: string: string; namename: string: string } };
}>({ idid: string: 'fetch-user', eventsevents: readonly "fetch-user"[]: ['fetch-user'], concurrencyconcurrency?: ConcurrencyStrategy | undefined
Concurrency strategy applied across handler runs (default: `'take-latest'`). - `take-latest` — new run aborts the previous (`signal.aborted` becomes true). - `take-every` — every run proceeds independently, no aborts. - `take-first` / `exhaust` — new runs are skipped while one is still in flight. - `queue` — new runs wait for the previous to finish (serialized). - `sync` — like `take-every`; provided as a documentation marker.
: 'take-latest',
async handler
handler: TriggerHandler<{
    events: {
        "fetch-user": string;
    };
    actions: {
        setUser: {
            id: string;
            name: string;
        };
    };
}, never>
({ event
event: {
    readonly name: "fetch-user";
    readonly payload: string;
}
, actions
actions: ActionsCtx<{
    setUser: {
        id: string;
        name: string;
    };
}>
, signalsignal: AbortSignal }) {
const resconst res: Response = await fetchfunction fetch(input: string | URL | Request, init?: RequestInit): Promise<Response> (+1 overload)
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/fetch)
(`/api/users/${event
event: {
    readonly name: "fetch-user";
    readonly payload: string;
}
.payloadpayload: string}`, { signalRequestInit.signal?: AbortSignal | null | undefined
An AbortSignal to set request's signal.
});
if (signalsignal: AbortSignal.abortedAbortSignal.aborted: boolean
The **`aborted`** read-only property returns a value that indicates whether the asynchronous operations the signal is communicating with are aborted (`true`) or not (`false`). [MDN Reference](https://developer.mozilla.org/docs/Web/API/AbortSignal/aborted)
) return;
actions
actions: ActionsCtx<{
    setUser: {
        id: string;
        name: string;
    };
}>
.setUser
setUser?: ((payload: {
    id: string;
    name: string;
}) => void) | undefined
?.(await resconst res: Response.jsonBody.json(): Promise<any>
[MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json)
());
}, });
import { createRuntimefunction createRuntime(options?: RuntimeOptions): Runtime, createTriggerfunction createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
} from '@triggery/core';
const runtimeconst runtime: Runtime = createRuntimefunction createRuntime(options?: RuntimeOptions): Runtime({ inspectorinspector?: InspectorOption | undefined
Enable / disable the per-run inspector. See {@link InspectorOption } .
: { devdev?: boolean | undefined: true, prodprod?: boolean | undefined: false } });
const trigger
const trigger: Trigger<{
    events: {
        hi: void;
    };
}>
= createTrigger
createTrigger<{
    events: {
        hi: void;
    };
}>(config: CreateTriggerConfig<{
    events: {
        hi: void;
    };
}>, runtime?: Runtime): Trigger<{
    events: {
        hi: void;
    };
}>
Imperative form. Pass the trigger config; returns a live `Trigger<S>` registered with the runtime. The chainable builder form lives in the `@triggery/core/builder` subpath — import from there if you want `createTrigger<S>().require(...).handle(...)` with auto-narrowing.
<{ events
events: {
    hi: void;
}
: { hihi: void: void } }>(
{ idid: string: 'demo:hi', eventsevents: readonly "hi"[]: ['hi'], handler
handler: TriggerHandler<{
    events: {
        hi: void;
    };
}, never>
() {
consolevar console: Console.logConsole.log(...data: any[]): void
The **`console.log()`** static method outputs a message to the console. [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static)
('hi');
}, }, runtimeconst runtime: Runtime, );

Calling createTrigger<S>() with no arguments returns a TriggerBuilder<S>. The builder accumulates configuration through chainable methods and finalizes with .handle(handler) — at which point the handler sees required conditions narrowed to NonNullable<...>. Import from @triggery/core/builder — the subpath keeps the builder machinery out of the main bundle.

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

const messageTrigger = createTrigger<{
  events:     { 'new-message': { author: string; text: string } };
  conditions: { user: { id: string }; settings: { sound: boolean } };
  actions:    { showToast: { title: string }; playSound: void };
}>()
  .id('message-received')
  .events(['new-message'])
  .conditions({ user: null, settings: null })
  .require('user', 'settings')
  .handle(({ event, conditions, actions }) => {
    // conditions.user: { id: string }      (no `!`, no early-return guard)
    // conditions.settings: { sound: boolean }
    actions.showToast?.({ title: conditions.user.id });
    if (conditions.settings.sound) actions.playSound?.();
  });

Methods:

MethodDescription
.id(string)Required. Same as the imperative id field.
.events([...])Required. Same as the imperative events field.
.conditions({ ... })Optional inline values, same shape as the imperative conditions: field.
.require(...keys)Add required condition keys. Calling .require('a').require('b').require('a', 'b'). Narrows conditions.<key> to NonNullable<...> in .handle.
.schedule('sync' | 'microtask')Override the scheduler.
.concurrency(strategy)Override the concurrency strategy.
.scope(string)Set the scope id (for <TriggerScope> isolation).
.handle(handler)Finalize and register the trigger with the default runtime. Returns Trigger<S>.

The chained order doesn’t matter; .handle validates that id and events were set.

Returned by trigger.action(name). A typed multi-subscriber channel for one action of the trigger — the v0.10 replacement for the v0.9 Set<callback> + for-of fan-out pattern.

type ActionChannel<P> = {
  subscribe(cb: (payload: P) => void): () => void;
  readonly subscribed: number;
};

Channel subscribers and any runtime.registerAction(...) handler for the same key both fire on every emit — the channel path is additive, not a replacement. The channel itself is cached per (trigger, name) so repeat calls return the same object.

const toast = messageTrigger.action('showToast');
const unsubA = toast.subscribe((p) => console.log('A', p));
const unsubB = toast.subscribe((p) => console.log('B', p));
// channel.subscribed === 2
unsubA();
// channel.subscribed === 1