
Code-First TypeScript Automation
Build powerful workflows and state machines with world-class autocomplete and type inference. No YAML, no drag-and-drop. Just TypeScript.
Why Klonk?
Built for developers who want the full power of TypeScript in their automation pipelines
Type-Safe & Autocompleted
Leverage TypeScript's inference for world-class developer experience. Every step is strongly typed with intelligent autocomplete.
Code-First
Define automations directly in TypeScript. No YAML, no drag-and-drop. Just clean, testable, version-controlled code.
Composable & Extensible
Build on simple primitives (Task, Trigger) to create reusable components. Extend with your own integrations easily.
Event-Driven Workflows
Connect triggers to task playlists for powerful automation. Perfect for file watchers, webhooks, and scheduled jobs.
State Machines
Build complex, stateful agents with finite state machines. Each state has its own logic and conditional transitions.
Flexible Execution
Run machines synchronously for request/response patterns, or asynchronously as long-running background processes.
Core Concepts
Simple, composable primitives that work together
Task
The smallest unit of work. An abstract class with validateInput() and run() methods. Returns a Railroad type for error handling without exceptions.
Playlist
A sequence of Tasks executed in order. Each task has type-safe access to outputs of all previous tasks through the builder function.
Trigger
Event source that kicks off a Workflow. Extend for file system events, webhooks, database changes, queues, or any event source.
Workflow
Connects Triggers to a Playlist. When a trigger fires, the workflow runs the playlist with the event data as input.
Machine
Finite state machine with StateNodes. Each node has a Playlist and conditional Transitions. Carries mutable stateData throughout execution.
StateNode
A state in a Machine with its own Playlist, Transitions to other states, and retry rules for failed transitions.
Compositional Architecture
Simple primitives that compose into powerful automation systems
Workflow Composition
Machine Composition
Klonk provides two primary composition patterns for building automation systems:
Workflow Pattern
A Workflow connects one or more Triggers to a single Playlist, which contains a sequence of Tasks.
Machine Pattern
A Machine orchestrates one or more StateNodes. Each StateNode contains a Playlist of Tasks.
See It In Action
Type-safe workflows with intelligent autocomplete
Machines are ideal for building complex, stateful agents. This example shows a simple AI agent that takes a user's query, refines it, performs a web search, and then generates a final response.
The Machine manages a StateData object. Each StateNode's Playlist can modify this state, and the Transitions between states can use it to decide which state to move to next.1import { Machine, StateNode } from "@fkws/klonk";
2import { OpenRouterClient } from "./tasks/common/OpenrouterClient";
3import { Model } from "./tasks/common/models";
4import { TABasicTextInference } from "./tasks/TABasicTextInference";
5import { TASearchOnline } from "./tasks/TASearchOnline";
6
7type StateData = {
8 input: string;
9 output?: string;
10 model?: Model;
11 refinedInput?: string;
12 searchTerm?: string;
13 searchResults?: {
14 results: {
15 url: string;
16 title: string;
17 content: string;
18 raw_content?: string | undefined;
19 score: string;
20 }[];
21 query: string;
22 answer?: string | undefined;
23 images?: string[] | undefined;
24 follow_up_questions?: string[] | undefined;
25 response_time: string;
26 };
27 finalResponse?: string;
28};
29
30const client = new OpenRouterClient(process.env.OPENROUTER_API_KEY!);
31
32const webSearchAgent = Machine.create<StateData>()
33 .addState(
34 StateNode.create<StateData>()
35 .setIdent("refine_and_extract")
36 .setPlaylist((p) =>
37 p // Builder function allows complex types to be assembled!
38 .addTask(new TABasicTextInference("refine", client), (state, outputs) => {
39 // This function constructs the INPUT of the task from the state and outputs of previous tasks
40 const input = state.input;
41 const model = state.model ? state.model : "openai/gpt-5";
42 const instructions = `You are a prompt refiner. Any prompts you receive, you will refine to `
43 + `improve LLM performance. Break down the prompt by Intent, Mood, and Instructions. `
44 + `Do NOT reply or answer the user's message! ONLY refine the prompt.`;
45 return {
46 inputText: input,
47 model: model,
48 instructions: instructions,
49 };
50 })
51 .addTask(new TABasicTextInference("extract_search_terms", client), (state, outputs) => {
52 const input = `Original request: ${state.input}\n\nRefined prompt: ${state.refinedInput}`;
53 const model = state.model ? state.model : "openai/gpt-5";
54 const instructions = `You will receive the original user request AND an LLM refined version of the prompt. `
55 + `Please use both to extract one short web search query that will retrieve useful results.`;
56 return {
57 inputText: input,
58 model: model,
59 instructions: instructions,
60 };
61 })
62 .finally((state, outputs) => {
63 // The finally block allows the playlist to react to the last task and to modify state data before the run ends.
64 if (outputs.refine.success) {
65 state.refinedInput = outputs.refine.data.text;
66 } else {
67 state.refinedInput = "Sorry, an error occurred: " + outputs.refine.error;
68 }
69
70 if (outputs.extract_search_terms.success) {
71 state.searchTerm = outputs.extract_search_terms.data.text;
72 }
73 }),
74 )
75 .retryLimit(3) // Simple retry rule setters. Also includes .preventRetry() to disable
76 // retries entirely and .retryDelayMs(delayMs) to set the delay between retries.
77 // Default is infinite retries at 1000ms delay.
78 .addTransition({
79 to: "search_web", // Transitions refer to states by their ident.
80 condition: async (stateData: StateData) => (stateData.searchTerm ? true : false),
81 weight: 2, // Weight determines the order in which transitions are tried. Higher weight = higher priority.
82 })
83 .addTransition({
84 to: "generate_response",
85 condition: async (stateData: StateData) => true,
86 weight: 1,
87 }),
88 {
89 initial: true,
90 }, // The machine needs an initial state.
91 )
92 .addState(
93 StateNode.create<StateData>()
94 .setIdent("search_web")
95 .setPlaylist((p) =>
96 p
97 .addTask(new TASearchOnline("search"), (state, outputs) => {
98 return {
99 query: state.searchTerm!, // We are sure that the searchTerm is not undefined because of the
100 // transition condition.
101 };
102 })
103 .finally((state, outputs) => {
104 if (outputs.search.success) {
105 state.searchResults = outputs.search.data;
106 }
107 }),
108 )
109 .addTransition({
110 to: "generate_response",
111 condition: async (stateData: StateData) => true,
112 weight: 1,
113 }),
114 )
115 .addState(
116 StateNode.create<StateData>()
117 .setIdent("generate_response")
118 .setPlaylist((p) =>
119 p
120 .addTask(new TABasicTextInference("generate_response", client), (state, outputs) => {
121 return {
122 inputText: state.input,
123 model: state.model ? state.model : "openai/gpt-5",
124 instructions:
125 "You will receive a user request and a refined prompt. There may also be search results. "
126 + "Based on the information, please write a professional response to the user's request.",
127 };
128 })
129 .finally((state, outputs) => {
130 if (outputs.generate_response.success) {
131 state.finalResponse = outputs.generate_response.data.text;
132 } else {
133 state.finalResponse = "Sorry, an error occurred: " + outputs.generate_response.error;
134 }
135 }),
136 ),
137 )
138 .addLogger(pino()) // If you add a logger, Klonk will call its
139 // info(), error(), debug(), fatal(), warn(), and trace()
140 // methods to emit logs. Built for pino, but you can pass any fitting logger object.
141 .finalize({
142 // Finalize your machine to make it ready to run.
143 // If you don't provide an ident, a uuidv4 will be generated for it.
144 ident: "web-search-agent",
145 });
146
147// ------------- EXECUTION: -------------
148
149const state: StateData = {
150 // The state object is mutable and is passed to the machine and playlists.
151 input: "How do I update AMD graphic driver?",
152 model: "openai/gpt-4o-mini",
153};
154
155// The .run() method executes the machine until it reaches a terminal condition
156// based on the selected mode. For example, 'roundtrip' stops when it returns
157// to the initial state. The original state object is also mutated.
158const finalState = await webSearchAgent.run(state, { mode: 'roundtrip' });
159
160console.log(finalState.finalResponse); // The final state is returned.
161// Or simply:
162console.log(state.finalResponse); // original state object is also mutated.Get Started
Install Klonk and start building type-safe automations
# Using npm
npm install @fkws/klonk
# Using bun
bun add @fkws/klonk