Developing a Calculator Skill for OpenClaw
Clawpedia · For Humans
Learn skill development fundamentals by building a fully functional calculator skill for OpenClaw.
Developing a Calculator Skill for OpenClaw
Building a calculator skill is one of the best beginner projects for learning OpenClaw skill development. It covers all the fundamentals — parameter handling, input validation, output formatting, and error handling — in a simple, testable package.
---
What We Will Build
A skill that:
| Feature | Description |
|---|
| Basic math | Addition, subtraction, multiplication, division |
|---|
| Advanced math | Powers, roots, percentages, factorials |
|---|
| Unit conversion | Length, weight, temperature, currency |
|---|
| Expression parsing | Evaluate full math expressions |
|---|
| History | Remember recent calculations |
|---|
---
Step 1: Create the Skill Structure
mkdir -p ~/.openclaw/skills/calculator
cd ~/.openclaw/skills/calculator
Manifest
{
"name": "calculator",
"version": "1.0.0",
"description": "Mathematical calculations and unit conversions",
"author": "Your Name",
"triggers": ["calculate", "calc", "math", "convert"],
"parameters": {
"expression": {
"type": "string",
"description": "Math expression to evaluate",
"required": true
}
}
}
---
Step 2: Build the Math Engine
// math-engine.js
// Safe math expression evaluator (no eval!)
export function evaluateExpression(expr) {
// Tokenize
const tokens = tokenize(expr);
// Parse and evaluate using recursive descent
const result = parseExpression(tokens, 0);
return result.value;
}
function tokenize(expr) {
const tokens = [];
let i = 0;
const cleaned = expr.replace(/\s+/g, "");
while (i < cleaned.length) {
if (/[0-9.]/.test(cleaned[i])) {
let num = "";
while (i < cleaned.length && /[0-9.]/.test(cleaned[i])) {
num += cleaned[i++];
}
tokens.push({ type: "number", value: parseFloat(num) });
} else if ("+-*/^%()!".includes(cleaned[i])) {
tokens.push({ type: "operator", value: cleaned[i++] });
} else {
// Function names (sqrt, sin, cos, etc.)
let name = "";
while (i < cleaned.length && /[a-z]/i.test(cleaned[i])) {
name += cleaned[i++];
}
tokens.push({ type: "function", value: name.toLowerCase() });
}
}
return tokens;
}
// Supported functions
const FUNCTIONS = {
sqrt: Math.sqrt,
sin: Math.sin,
cos: Math.cos,
tan: Math.tan,
abs: Math.abs,
log: Math.log10,
ln: Math.log,
round: Math.round,
floor: Math.floor,
ceil: Math.ceil
};
function parseExpression(tokens, pos) {
// Implementation of recursive descent parser
// Handles operator precedence: +- < */ < ^ < unary < functions
let left = parseTerm(tokens, pos);
while (left.pos < tokens.length &&
tokens[left.pos]?.type === "operator" &&
"+-".includes(tokens[left.pos].value)) {
const op = tokens[left.pos].value;
const right = parseTerm(tokens, left.pos + 1);
left = {
value: op === "+" ? left.value + right.value : left.value - right.value,
pos: right.pos
};
}
return left;
}
function parseTerm(tokens, pos) {
let left = parseFactor(tokens, pos);
while (left.pos < tokens.length &&
tokens[left.pos]?.type === "operator" &&
"*/".includes(tokens[left.pos].value)) {
const op = tokens[left.pos].value;
const right = parseFactor(tokens, left.pos + 1);
if (op === "/" && right.value === 0) throw new Error("Division by zero");
left = {
value: op === "*" ? left.value * right.value : left.value / right.value,
pos: right.pos
};
}
return left;
}
function parseFactor(tokens, pos) {
if (tokens[pos]?.type === "number") {
return { value: tokens[pos].value, pos: pos + 1 };
}
if (tokens[pos]?.type === "operator" && tokens[pos].value === "(") {
const inner = parseExpression(tokens, pos + 1);
return { value: inner.value, pos: inner.pos + 1 }; // skip )
}
if (tokens[pos]?.type === "function") {
const fn = FUNCTIONS[tokens[pos].value];
if (!fn) throw new Error(`Unknown function: ${tokens[pos].value}`);
const arg = parseFactor(tokens, pos + 1);
return { value: fn(arg.value), pos: arg.pos };
}
throw new Error(`Unexpected token at position ${pos}`);
}
---
Step 3: Add Unit Conversion
// conversions.js
const CONVERSIONS = {
// Length
km_to_miles: (v) => v * 0.621371,
miles_to_km: (v) => v * 1.60934,
m_to_feet: (v) => v * 3.28084,
feet_to_m: (v) => v / 3.28084,
cm_to_inches: (v) => v / 2.54,
inches_to_cm: (v) => v * 2.54,
// Weight
kg_to_lbs: (v) => v * 2.20462,
lbs_to_kg: (v) => v / 2.20462,
g_to_oz: (v) => v / 28.3495,
oz_to_g: (v) => v * 28.3495,
// Temperature
c_to_f: (v) => v * 9/5 + 32,
f_to_c: (v) => (v - 32) * 5/9,
c_to_k: (v) => v + 273.15,
k_to_c: (v) => v - 273.15,
// Data
gb_to_mb: (v) => v * 1024,
mb_to_gb: (v) => v / 1024,
tb_to_gb: (v) => v * 1024,
};
export function convert(value, from, to) {
const key = `${from.toLowerCase()}_to_${to.toLowerCase()}`;
const fn = CONVERSIONS[key];
if (!fn) {
throw new Error(`Unknown conversion: ${from} to ${to}`);
}
return {
input: `${value} ${from}`,
output: `${fn(value).toFixed(4)} ${to}`,
formula: key.replace(/_/g, " ")
};
}
export function listConversions() {
return Object.keys(CONVERSIONS).map(key => {
const [from, , to] = key.split("_");
return { from: from.toUpperCase(), to: to.toUpperCase() };
});
}
---
Step 4: Build the Main Skill
// index.js
import { evaluateExpression } from "./math-engine.js";
import { convert, listConversions } from "./conversions.js";
export default {
name: "calculator",
description: "Mathematical calculations and unit conversions",
execute: async (context) => {
const input = context.parameters.expression;
// Detect conversion requests
const convMatch = input.match(
/(\d+\.?\d*)\s*(\w+)\s+(?:to|in|as)\s+(\w+)/i
);
if (convMatch) {
try {
const result = convert(
parseFloat(convMatch[1]),
convMatch[2],
convMatch[3]
);
return `🔄 **Conversion**\n${result.input} = **${result.output}**`;
} catch (e) {
return `❌ ${e.message}\n\nAvailable conversions:\n` +
listConversions().map(c => ` ${c.from} ↔ ${c.to}`).join("\n");
}
}
// Evaluate math expression
try {
const result = evaluateExpression(input);
const formatted = Number.isInteger(result)
? result.toString()
: result.toFixed(6).replace(/\.?0+$/, "");
return `🧮 **Result**\n\`${input}\` = **${formatted}**`;
} catch (e) {
return `❌ Could not evaluate: ${e.message}\n\n` +
`Try:\n` +
`• Math: \`2 + 3 * 4\`, \`sqrt(144)\`, \`2^10\`\n` +
`• Convert: \`100 km to miles\`, \`72 F to C\``;
}
}
};
---
Step 5: Add Tests
// test.js
import { evaluateExpression } from "./math-engine.js";
import { convert } from "./conversions.js";
const tests = [
// Basic math
["2 + 3", 5],
["10 - 4", 6],
["3 * 7", 21],
["20 / 4", 5],
// Order of operations
["2 + 3 * 4", 14],
["(2 + 3) * 4", 20],
// Functions
["sqrt(144)", 12],
["abs(-5)", 5],
];
let passed = 0;
for (const [expr, expected] of tests) {
const result = evaluateExpression(expr);
const ok = Math.abs(result - expected) < 0.0001;
console.log(`${ok ? "✅" : "❌"} ${expr} = ${result} (expected ${expected})`);
if (ok) passed++;
}
console.log(`\n${passed}/${tests.length} tests passed`);
Run tests:
node test.js
---
Step 6: Install and Use
openclaw skill install ~/.openclaw/skills/calculator
# Test
openclaw chat "Calculate 2^10"
openclaw chat "Convert 100 km to miles"
openclaw chat "What is sqrt(2) * 3.14?"
---
Troubleshooting
"Unknown function" Error
Only supported functions work: sqrt, sin, cos, tan, abs, log, ln, round, floor, ceil. Add more in the FUNCTIONS object.
Floating Point Precision
JavaScript floating point can produce results like 0.1 + 0.2 = 0.30000000000000004. The skill rounds output to 6 decimal places to minimize this.
Complex Expressions Fail
The parser handles standard math notation. For very complex expressions, extend the parser or use a library like math.js.
---
Summary
The calculator skill demonstrates all fundamental skill development patterns: input parsing, business logic, error handling, and formatted output. It is a great foundation for more complex skills and a practical everyday tool.
Related Articles
- Deploying a Custom OpenClaw Skill: Best Practices — Learn deployment strategies and best practices for shipping reliable OpenClaw skills to production.
- How to Prepare for the $3 Trillion AI Infrastructure Shift — Morgan Stanley predicts $3 trillion in AI infrastructure spending by 2028. Learn what this means for developers, startups, and enterprises building with AI.
- AI Agents Running Your Company: Lessons from Ramp's $32B Playbook — Ramp is one of the most AI-native companies at $32B valuation. Learn how they use agents for customer research, data analysis, and product development.
- Building Voice-Enabled AI Agents with Real-Time Speech APIs — Develop real-time voice-enabled AI agents using modern speech APIs. Learn best practices, architecture, and code examples for seamless voice interaction.
- Replit Agent — Building and Deploying in One Browser Tab — In 2026, the friction between writing code and seeing it live has nearly vanished. While the early 2020s were defined by local IDEs and complex CI/CD pipelines, the current landscape favors "Integrated Development Environments" that actuall