
Research
Supply Chain Attack on Axios Pulls Malicious Dependency from npm
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.
A lightweight, composable JavaScript framework for building LLM-powered agent flows
A lightweight, composable JavaScript framework for building LLM-powered agent flows.
Flowlite is a modern JavaScript framework that makes it easy to build structured, chainable workflows for LLM-powered agents. It provides a clean, concise syntax for defining complex flows while maintaining readability and flexibility.
+---------------+ +---------------+ +---------------+
| Tool Classes | | Flow Builder | | Flow Executor |
| | | | | |
| - LLMTool |<----->| - Flow.create |<----->| - run() |
| - APITool | | - next() | | - execute() |
| - Tool | | - on() | | |
+---------------+ +---------------+ +---------------+
^ ^ ^
| | |
v v v
+---------------+ +---------------+ +---------------+
| Memory System | | Logging System| | Error Handling|
| | | | | |
| - store() | | - LogLevel | | - retry |
| - retrieve() | | - setLogger() | | - fallback |
+---------------+ +---------------+ +---------------+
Key features:
npm install flowlite
Let's start with a simple "hello world" example:
import { Flow } from 'flowlite';
// Ultra-compact flow definition
export const greet = Flow.create({ name: 'greet', input: [{ name: 'name', type: 'string', description: 'Name to greet' }] })
.next(({ name = 'World' }) => {
console.log(`Hello, ${name}!`);
return { greeted: true };
});
// Run the flow
greet.run({ name: 'Flowlite' });
// Output: Hello, Flowlite!
The core building blocks of Flowlite are Nodes (units of work) and Flows (sequences of nodes):
Flow Execution
|
v
+------------+ +------+ +------------+ +------------+
| Input |--->| Node |--->| Node |--->| Output |
| Parameters | | 1 | | 2 | | State |
+------------+ +------+ +------------+ +------------+
^ ^
| |
+------------+ +------------+
| Tool | | Tool |
| Definition | | Definition |
+------------+ +------------+
import { Flow, param, ParamType } from 'flowlite';
// Ultra-compact tool definitions with inline metadata
const fetchData = async ({ id }) => {
const data = await fetch(`https://api.example.com/data/${id}`);
return { data: await data.json() };
};
fetchData.metadata = { name: 'fetchData', input: [param('id', ParamType.STRING, 'ID to fetch')] };
const processData = ({ data }) => ({
processed: data.map(item => item.value * 2),
status: 'complete'
});
processData.metadata = { name: 'processData' };
// Compact flow definition
export const dataFlow = Flow.create({ name: 'dataFlow' })
.next(fetchData)
.next(processData);
dataFlow.run({ id: '12345' });
Flows can branch based on the output of nodes:
+-------------+
| Input State |
+-------------+
|
v
+-------------+
| Branch Node |
+-------------+
|
+------------+------------+
| | |
v v v
+-------------+ +----------+ +------------+
| "question" | | "request"| | "statement"|
| Branch | | Branch | | Branch |
+-------------+ +----------+ +------------+
| | |
v v v
+-------------+ +----------+ +------------+
| Question | | Request | | Statement |
| Handler | | Handler | | Handler |
+-------------+ +----------+ +------------+
| | |
+------------+------------+
|
v
+-------------+
| Final State |
+-------------+
import { Flow } from 'flowlite';
// Ultra-compact branching flow with inline functions
export const conversationFlow = Flow.create({ name: 'conversation', input: [param('text', ParamType.STRING, 'User input')] })
.next(({ text }) => {
if (text.includes('question')) return 'question';
if (text.includes('request')) return 'request';
return 'statement';
})
.on('question', ({ text }) => ({ response: `To answer your question: ${text}` }))
.on('request', ({ text }) => ({ response: `I'll process your request: ${text}` }))
.on('statement', ({ text }) => ({ response: `I acknowledge your statement: ${text}` }));
const result = await conversationFlow.run({ text: 'Can you answer a question for me?' });
console.log(result.response);
// Output: To answer your question: Can you answer a question for me?
Flowlite includes built-in tools for working with LLMs:
import { Flow, LLMTool, param, ParamType } from 'flowlite';
// Ultra-compact LLM tool using class expression
const generateResponse = new class extends LLMTool {
constructor() {
super({
name: 'generateResponse',
description: 'Generate a response to user input',
input: [param('input', ParamType.STRING, 'User input')]
});
}
buildPrompt({ input }) {
return `You are a helpful assistant. User: ${input}`;
}
processResponse(response) {
return { response };
}
}();
// Compact flow with direct tool usage
export const chatFlow = Flow.create({ name: 'chat' })
.next(async (state) => generateResponse.call(state));
const result = await chatFlow.run({ input: 'Tell me a joke about programming' });
console.log(result.response);
Flowlite provides a powerful class-based architecture for creating tools with inheritance:
+------------------------+
| Tool |
|------------------------|
| + metadata |
| + execute() |
| + call() |
| + withApiKey() |
| + setLogLevel() |
+------------------------+
^
|
|
+------------------------+ +------------------------+
| LLMTool | | APITool |
|------------------------| |------------------------|
| + buildPrompt() | | + fetchWithApiKey() |
| + processResponse() | | + handleApiError() |
| + withModel() | | + withRetry() |
+------------------------+ +------------------------+
import { LLMTool, APITool, param, ParamType } from 'flowlite';
// Ultra-compact LLM tool using class expression
const summarize = new class extends LLMTool {
constructor() {
super({
name: 'summarize',
description: 'Summarizes text content',
input: [
param('text', ParamType.STRING, 'Text to summarize'),
param('maxLength', ParamType.NUMBER, 'Maximum length of summary', { optional: true })
]
});
}
buildPrompt({ text, maxLength = 100 }) {
return `Summarize the following text in ${maxLength} words or less:
${text}
Provide a concise summary that captures the key points.`;
}
processResponse(response) {
return { summary: response };
}
}();
// Ultra-compact API tool using class expression
const getWeather = new class extends APITool {
constructor() {
super({
name: 'getWeather',
description: 'Gets current weather for a location',
input: [param('location', ParamType.STRING, 'City name or coordinates')]
});
this.withApiKey('WEATHER_API_KEY');
}
async execute({ location }) {
const url = `https://api.weather.com/v1/current?location=${encodeURIComponent(location)}`;
const response = await this.fetchWithApiKey(url);
const data = await response.json();
return {
temperature: data.current.temperature,
conditions: data.current.conditions,
city: data.location.name
};
}
}();
// Ultra-compact weather flow
export const weatherFlow = Flow.create({ name: 'weather', input: [param('city', ParamType.STRING, 'City to check weather for')] })
.next(async ({ city }) => {
const weather = await getWeather.call({ location: city });
const summary = await summarize.call({
text: `The weather in ${city} is ${weather.temperature}°C and ${weather.conditions}.`
});
return { weatherSummary: summary.summary };
});
Flowlite includes a comprehensive logging system for debugging and performance monitoring:
+----------------+
| LogLevel |
|----------------|
| NONE |
| ERROR |
| WARN |
| INFO |
| DEBUG |
| TRACE |
+----------------+
^
|
+-----------------------------+
| |
+-----------------+ +-----------------+
| Flow | | Tool |
|-----------------| |-----------------|
| + setLogLevel() |<--------->| + setLogLevel() |
| + setLogger() | | + setLogger() |
| + debug() | | + debug() |
| + info() | | + info() |
| + warn() | | + warn() |
| + error() | | + error() |
+-----------------+ +-----------------+
import { Flow, LogLevel, LLMTool } from 'flowlite';
// Configure log levels with chainable API
const summarizer = new class extends LLMTool {
constructor() {
super({ name: 'summarize', description: 'Summarizes text' });
}
buildPrompt({ text }) {
return `Summarize: ${text}`;
}
}()
.setLogLevel(LogLevel.DEBUG)
.setLogger(console);
// Track performance statistics
const stats = summarizer.getStats();
console.log(`Executions: ${stats.executionCount}, Avg time: ${stats.averageExecutionTime}ms`);
// Reset statistics
summarizer.resetStats();
Store and retrieve data across interactions:
import { Flow } from 'flowlite';
import { createMemoryStore } from 'flowtools';
// Create a memory store
const memory = createMemoryStore();
// Ultra-compact memory flow
export const memoryFlow = Flow.create({ name: 'memoryFlow' })
.next(({ name }) => {
memory({ key: 'userName', value: name, action: 'set' });
return { remembered: true };
})
.next(() => {
const name = memory('userName') || 'stranger';
return { greeting: `Hello again, ${name}!` };
});
// First run to store the name
await memoryFlow.run({ name: 'Alice' });
// Second run to retrieve the name
const result = await memoryFlow.run({});
console.log(result.greeting);
// Output: Hello again, Alice!
Register and use tools across different flows:
import { Flow, Tool, param, ParamType } from 'flowlite';
// Ultra-compact tool definition
const helloWorldTool = new class extends Tool {
constructor() {
super({
name: 'helloWorld',
description: 'Says hello to someone',
input: [param('name', ParamType.STRING, 'Name to greet', { optional: true })]
});
}
execute({ name = 'World' }) {
return `Hello, ${name}!`;
}
}();
// Ultra-compact planner flow
export const plannerFlow = Flow.create({ name: 'planner' })
.next(({ needsGreeting, name }) => needsGreeting
? { result: helloWorldTool.call({ name }), toolUsed: 'helloWorld' }
: { result: 'No greeting needed', toolUsed: 'none' }
);
const result = await plannerFlow.run({
needsGreeting: true,
name: 'Flowlite User'
});
console.log(result);
// Output: { result: 'Hello, Flowlite User!', toolUsed: 'helloWorld' }
Run multiple tasks in parallel:
import { Flow, param, ParamType } from 'flowlite';
// Ultra-compact parallel flow
export const userFlow = Flow.create({ name: 'userFlow', input: [param('userId', ParamType.STRING, 'User ID to fetch')] })
.next(async ({ userId }) => {
// Run multiple fetches in parallel
const [userData, userPosts] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()),
fetch(`/api/users/${userId}/posts`).then(r => r.json())
]);
// Return combined results
return {
user: {
...userData,
posts: userPosts
}
};
});
const result = await userFlow.run({ userId: '12345' });
console.log(result.user);
Flowlite provides several powerful patterns for controlling the flow of execution. Here are some simple examples demonstrating the most common patterns:
Branch your flow based on conditions or decisions:
+---------------+
| Input State |
+---------------+
|
v
+---------------+
| Decision Node |
+---------------+
|
+------------+------------+
| | |
v v v
+---------------+ +----------+ +------------+
| Condition A | |Condition B| |Condition C|
+---------------+ +----------+ +------------+
| | |
v v v
+---------------+ +----------+ +------------+
| Handler A | | Handler B | | Handler C |
+---------------+ +----------+ +------------+
| | |
+------------+------------+
|
v
+---------------+
| Final Result |
+---------------+
import { Flow, param, ParamType } from 'flowlite';
// Ultra-compact branching flow
export const classifyText = Flow.create({
name: 'classifyText',
input: [param('text', ParamType.STRING, 'Text to classify')]
})
.next(({ text }) => {
// Determine the category based on content
if (text.includes('question')) return 'question';
if (text.includes('complaint')) return 'complaint';
return 'feedback';
})
.on('question', ({ text }) => ({
response: `I'll answer your question: ${text}`,
category: 'question'
}))
.on('complaint', ({ text }) => ({
response: `I'm sorry about your issue: ${text}`,
category: 'complaint'
}))
.on('feedback', ({ text }) => ({
response: `Thank you for your feedback: ${text}`,
category: 'feedback'
}));
// Usage
const result = await classifyText.run({ text: "I have a question about my order" });
console.log(result.response); // "I'll answer your question: I have a question about my order"
Create loops to process items or repeat operations until a condition is met:
+---------------+
| Input Items |
+---------------+
|
v
+---------------+
| Initialize |
| Counter |
+---------------+
|
v
+-----> +---------------+
| | Check if Done |
| +---------------+
| |
| v
| +---------------+
| | More Items? |
| +---------------+
| / \
| Yes / \ No
| / \
| v v
| +---------------+ +---------------+
| | Process Item | | Return Result |
| +---------------+ +---------------+
| |
| v
| +---------------+
| | Increment |
| | Counter |
| +---------------+
| |
+---------+
import { Flow, param, ParamType } from 'flowlite';
// Ultra-compact looping flow
export const processItems = Flow.create({
name: 'processItems',
input: [param('items', ParamType.ARRAY, 'Items to process')]
})
.next(({ items }) => {
// Initialize state for the loop
return {
items,
results: [],
currentIndex: 0
};
})
.next(function processLoop({ items, results, currentIndex }) {
// Check if we're done
if (currentIndex >= items.length) {
return { done: true, results };
}
// Process the current item
const item = items[currentIndex];
const processedItem = `Processed: ${item}`;
// Update state and continue the loop
return {
items,
results: [...results, processedItem],
currentIndex: currentIndex + 1,
done: false
};
})
.on(state => !state.done, function continueLoop(state) {
// Continue the loop by calling the process function again
return this.getNode('processLoop').execute(state);
})
.on(state => state.done, state => ({ finalResults: state.results }));
// Usage
const result = await processItems.run({ items: ['apple', 'banana', 'cherry'] });
console.log(result.finalResults);
// ["Processed: apple", "Processed: banana", "Processed: cherry"]
Execute multiple operations simultaneously for better performance:
+---------------+
| Input State |
+---------------+
|
v
+---------------+
| Split Tasks |
+---------------+
|
+------------+------------+
| | |
v v v
+---------------+ +----------+ +------------+
| Task A | | Task B | | Task C |
| (Parallel) | |(Parallel)| | (Parallel) |
+---------------+ +----------+ +------------+
| | |
v v v
+---------------+ +----------+ +------------+
| Result A | | Result B | | Result C |
+---------------+ +----------+ +------------+
| | |
+------------+------------+
|
v
+---------------+
| Combine |
| Results |
+---------------+
import { Flow, param, ParamType } from 'flowlite';
// Define some async operations
const fetchUserProfile = async (userId) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: `User ${userId}`, email: `user${userId}@example.com` };
};
const fetchUserPosts = async (userId) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 500));
return [
{ id: 1, title: `Post 1 by User ${userId}` },
{ id: 2, title: `Post 2 by User ${userId}` }
];
};
const fetchUserFollowers = async (userId) => {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 400));
return [userId + 1, userId + 2, userId + 3];
};
// Ultra-compact parallel processing flow
export const getUserData = Flow.create({
name: 'getUserData',
input: [param('userId', ParamType.NUMBER, 'User ID to fetch')]
})
.next(async ({ userId }) => {
// Execute all fetches in parallel
const [profile, posts, followers] = await Promise.all([
fetchUserProfile(userId),
fetchUserPosts(userId),
fetchUserFollowers(userId)
]);
// Combine the results
return {
userData: {
...profile,
posts,
followers,
followerCount: followers.length
}
};
});
// Usage
const result = await getUserData.run({ userId: 42 });
console.log(result.userData);
// { id: 42, name: "User 42", email: "user42@example.com", posts: [...], followers: [...], followerCount: 3 }
Implement a planning pattern where one node decides what actions to take next:
+---------------+
| Input Query |
+---------------+
|
v
+---------------+
| Analyze |
| Query |
+---------------+
|
v
+---------------+
| Create |
| Action Plan |
+---------------+
|
v
+---------------+
| Select Tools |
+---------------+
|
+------------+------------+
| | |
v v v
+---------------+ +----------+ +------------+
| Execute Tool A| |Execute B | |Execute C |
| (if needed) | |(if needed)| |(if needed)|
+---------------+ +----------+ +------------+
| | |
+------------+------------+
|
v
+---------------+
| Combine |
| Results |
+---------------+
import { Flow, LLMTool, param, ParamType } from 'flowlite';
// Define some tools that the planner can use
const calculator = (expression) => {
return { result: eval(expression) };
};
calculator.metadata = { name: 'calculator', description: 'Evaluates a math expression' };
const weatherLookup = (location) => {
return { weather: `Sunny and 72°F in ${location}` };
};
weatherLookup.metadata = { name: 'weatherLookup', description: 'Gets weather for a location' };
const translator = (text, language) => {
return { translation: `[${text}] translated to ${language}` };
};
translator.metadata = { name: 'translator', description: 'Translates text to another language' };
// Define a planner tool using LLMTool
const queryPlanner = new class extends LLMTool {
constructor() {
super({
name: 'planner',
description: 'Plans which tools to use for a query',
input: [param('query', ParamType.STRING, 'User query')]
});
}
buildPrompt({ query }) {
return `Determine which tool to use for this query: "${query}"
Available tools:
- calculator: For math calculations
- weatherLookup: For weather information
- translator: For translating text
Return one of: "calculator", "weather", "translate", or "unknown".`;
}
processResponse(response) {
const tool = response.trim().toLowerCase();
if (tool.includes('calculator')) return { tool: 'calculator' };
if (tool.includes('weather')) return { tool: 'weather' };
if (tool.includes('translate')) return { tool: 'translate' };
return { tool: 'unknown' };
}
}();
// Ultra-compact planning flow
export const assistantFlow = Flow.create({
name: 'assistant',
input: [param('query', ParamType.STRING, 'User query')]
})
.next(async ({ query }) => {
// Plan which tool to use
const { tool } = await queryPlanner.call({ query });
// Extract relevant information based on the tool
let result;
switch (tool) {
case 'calculator':
const expression = query.replace(/[^0-9+\-*/().]/g, '');
result = calculator(expression);
break;
case 'weather':
const location = query.replace(/weather|in|what's|the|like|how|is|it|in/gi, '').trim();
result = weatherLookup(location);
break;
case 'translate':
const match = query.match(/translate\s+"([^"]+)"\s+to\s+(\w+)/i);
if (match) {
result = translator(match[1], match[2]);
} else {
result = { error: "Couldn't parse translation request" };
}
break;
default:
result = { response: "I'm not sure how to help with that query." };
}
return {
originalQuery: query,
toolUsed: tool,
...result
};
});
// Usage
const result1 = await assistantFlow.run({ query: "What's 25 * 4 + 10?" });
console.log(result1); // { originalQuery: "What's 25 * 4 + 10?", toolUsed: "calculator", result: 110 }
const result2 = await assistantFlow.run({ query: "What's the weather in Miami?" });
console.log(result2); // { originalQuery: "What's the weather in Miami?", toolUsed: "weather", weather: "Sunny and 72°F in Miami" }
Here's a more complete example that demonstrates several Flowlite features:
+--------------------+ +--------------------+ +--------------------+
| User Query |---->| Location Extractor |---->| Weather API |
| "Weather in NYC?" | | LLMTool | | APITool |
+--------------------+ +--------------------+ +--------------------+
|
v
+--------------------+ +--------------------+
| Response Generator |<----| Memory Store |
| LLMTool | | (save location) |
+--------------------+ +--------------------+
|
v
+--------------------+
| Weather Report |
| (Final Response) |
+--------------------+
import { Flow, LLMTool, APITool, param, ParamType } from 'flowlite';
import { createMemoryStore } from 'flowtools';
// Create a memory store
const memory = createMemoryStore();
// Ultra-compact tools using class expressions
const locationExtractor = new class extends LLMTool {
constructor() {
super({
name: 'extractLocation',
description: 'Extract location from weather request',
input: [param('request', ParamType.STRING, 'Weather request text')]
});
}
buildPrompt({ request }) {
return `Extract the location from this weather request: "${request}"`;
}
processResponse(response) {
return { location: response.trim() };
}
}();
const weatherApi = new class extends APITool {
constructor() {
super({
name: 'getWeather',
description: 'Get weather data for a location',
input: [param('location', ParamType.STRING, 'Location to get weather for')]
});
}
async execute({ location }) {
const response = await fetch(`https://api.weather.com/${location}`);
return await response.json();
}
}();
const weatherReporter = new class extends LLMTool {
constructor() {
super({
name: 'generateWeatherReport',
description: 'Create a friendly weather report',
input: [
param('location', ParamType.STRING, 'Location'),
param('data', ParamType.OBJECT, 'Weather data')
]
});
}
buildPrompt({ location, data }) {
return `Create a friendly weather report for ${location} based on this data: ${JSON.stringify(data)}`;
}
}();
// Ultra-compact weather flow
export const weatherFlow = Flow.create({ name: 'weatherAssistant', input: [param('input', ParamType.STRING, 'User weather query')] })
.next(async ({ input }) => {
const { location } = await locationExtractor.call({ request: input });
const weatherData = await weatherApi.call({ location });
memory({ key: 'lastLocation', value: location, action: 'set' });
const response = await weatherReporter.call({ location, data: weatherData });
return { response };
});
// Run the flow
const result = await weatherFlow.run({
input: "What's the weather like in San Francisco today?"
});
console.log(result.response);
For more detailed documentation and examples, see:
Flowlite includes several example applications to demonstrate its capabilities:
A CLI tool for generating high-quality articles with AI assistance. Features include:
To try it out:
cd example_apps/article-writer
npm install
npm start
See the Article Writer README for more details.
Contributions are welcome! Please feel free to submit a Pull Request.
git checkout -b feature/amazing-featuregit commit -m 'Add some amazing feature'git push origin feature/amazing-featureMIT
FAQs
A lightweight, composable JavaScript framework for building LLM-powered agent flows
We found that flowlite demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
A supply chain attack on Axios introduced a malicious dependency, plain-crypto-js@4.2.1, published minutes earlier and absent from the project’s GitHub releases.

Research
Malicious versions of the Telnyx Python SDK on PyPI delivered credential-stealing malware via a multi-stage supply chain attack.

Security News
TeamPCP is partnering with ransomware group Vect to turn open source supply chain attacks on tools like Trivy and LiteLLM into large-scale ransomware operations.