Klonk Logo

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

Scroll down to see both patterns

Workflow Composition

Workflow
Trigger
1..n
Trigger
Playlist
Task
1..n
Task

Machine Composition

Machine
StateNode
1..n
StateNode
Playlist
Task
1..n
Task

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.
typescript
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

bash
# Using npm
npm install @fkws/klonk

# Using bun
bun add @fkws/klonk

Ready to automate?

Check out the documentation and examples on GitHub

View Documentation
npm Package