In five minutes you’ll have a single file that declares a scenario (“when a new message arrives and the user has notifications on — show a toast”), and three small components that plug into it. None of the components know about each other.
A React 18+/19, Solid 1.8+, or Vue 3.4+ project. (If you don’t have one yet, run pnpm dlx @triggery/cli create my-app to scaffold a minimal Vite project — see @triggery/cli.)
TypeScript is recommended but not required. Examples on this site assume TS.
import { createRuntime } from '@triggery/core';import { TriggerRuntimeProvider } from '@triggery/react';import { StrictMode } from 'react';import { createRoot } from 'react-dom/client';import { App } from './App';const runtime = createRuntime();createRoot(document.getElementById('root')!).render( <StrictMode> <TriggerRuntimeProvider runtime={runtime}> <App /> </TriggerRuntimeProvider> </StrictMode>,);
src/index.tsx
import { createRuntime } from '@triggery/core';import { TriggerRuntimeProvider } from '@triggery/solid';import { render } from 'solid-js/web';import { App } from './App';const runtime = createRuntime();render( () => ( <TriggerRuntimeProvider runtime={runtime}> <App /> </TriggerRuntimeProvider> ), document.getElementById('root')!,);
src/main.ts
import { createRuntime } from '@triggery/core';import { TriggerRuntimePlugin } from '@triggery/vue';import { createApp } from 'vue';import App from './App.vue';const runtime = createRuntime();createApp(App).use(TriggerRuntimePlugin, { runtime }).mount('#root');
You can omit the provider altogether — in that case Triggery uses a lazily created default runtime. The provider exists so testing, micro-frontends and multi-tenant setups can isolate runtimes.
Triggery’s house style is one trigger per file, suffixed .trigger.ts. The file reads top-to-bottom as a specification.
src/triggers/message.trigger.ts
import { createTrigger } from '@triggery/core/builder';type Settings = { sound: boolean; notifications: boolean };type Message = { author: string; text: string; channelId: string };export const messageTrigger = createTrigger<{ events: { 'new-message': Message }; conditions: { settings: Settings; activeChannelId: string | null }; actions: { showToast: { title: string; body: string }; playSound: 'beep' };}>() .id('message-received') .events(['new-message']) .require('settings') .handle(({ event, conditions, actions, check }) => { // Skip if the user is already looking at this conversation. if (conditions.activeChannelId === event.payload.channelId) return; // Skip if notifications are off — `check.is` narrows the value. if (!check.is('settings', s => s.notifications)) return; // Debounce sound so a burst of messages plays one beep. actions.debounce(800).playSound?.('beep'); actions.showToast?.({ title: event.payload.author, body: event.payload.text, }); });
The single inline-generic captures the whole port surface — three maps for events, conditions and actions. .require('settings') narrows conditions.settings to its non-null type inside .handle(...), so we read s.notifications without a ! or an early-return guard. The handler ctx is now strongly typed: try to fire a wrong event name or pass the wrong action payload and TypeScript stops you.
Mount the three components anywhere in the tree under your provider — they discover each other through the trigger, not through React/Solid/Vue context.
Click “send a fake message” in the Chat component. The notification layer logs (or pops a toast). Toggle settings.notifications off and it falls silent. That’s it — the whole scenario is one file, and every component is free to grow in isolation.