Blog

You've Seen AI Excel Agents. Here's How to Build Your Own.

Excel gives you Agent Mode. Google Sheets offers Gemini. GPT for Work can process 1,000 rows per minute, but it sends your data somewhere else. In this tutorial, we'll show you how to build a spreadsheet agent all of your own.

icon jspreadsheet

Published at 08/04/2026

Spreadsheet agents got real in 2026

Microsoft's Agent Mode in Excel became widely available in January 2026. Ask it to "clean this data, build a pivot table, and chart the results", and you'll likely see it carrying out all three steps without you having to touch a single cell.

Google answered with Gemini-powered analysis in Sheets, and GPT for Work now handles row processing at scale. Every one of these tools is good at what it does, but each one doubles as a black box that can fire your data to third-party servers, which means you're charged per seat and get zero control over the model used or how it reasons.

If you're creating a web app that includes a spreadsheet, you don't actually need someone else's agent. You need your own, running on your own infrastructure, using your preferred model, and doing exactly the job your users need. In this tutorial, we'll show you how to build one from scratch.

Diagram of a JavaScript spreadsheet agent: an AI node pulses on the left, tool calls (read_data, write_column, set_style, done) fire in sequence across the middle, and the spreadsheet on the right fills a Growth column and highlights declining rows in red

What makes an agent different from a cell function

If you've seen =AI() cell functions in Google Sheets or SpreadJS, you'll know the pattern: type =AI("classify this", A1) and the model returns a result into the cell. That's a useful function, but it's not an agent.

An agent can perform multiple steps in sequence without each step being laid out for it. You tell it to "analyze my sales data and highlight the underperformers," and the agent decides that it needs to:

  1. Read the spreadsheet data
  2. Calculate which products are below average
  3. Apply conditional formatting or write results into new cells
  4. Summarize what it found

The primary difference here is that a cell function can only do one thing per cell. An agent, on the other hand, can plan, execute, and refine data across an entire spreadsheet. This is what Microsoft calls "Agent Mode" — and it's what we're about to build.

The architecture

Three components:

A JavaScript spreadsheet that the agent can read from and write to programmatically. We'll use Jspreadsheet Pro (v12) — its formula engine, validations, fill handle, and XLSX import give the agent richer tools to work with than a plain data grid.

A large language model (LLM) that receives the spreadsheet data, a breakdown of the available tools, and a user prompt. Here, we'll run the AI locally using Ollama, but you can use any LLM API.

A tool layer that enables the LLM to perform specific operations: read cells, write cells, add formulas, insert rows, set styles. The agent chooses the tools based on your request.

This is the same pattern you'll find behind every AI agent framework, whether you're talking about CrewAI, LangChain, or Claude's tool use. The spreadsheet is simply the arena the agent operates in.

Step 1: Set up the spreadsheet

Kick off with a single Jspreadsheet Pro instance and a small sample dataset before setting the agent to work. Nothing fancy — just sales data with a dropdown, currency formatting, and the Pro toolbar so the agent's edits show up cleanly.

<link rel="stylesheet" href="https://jspreadsheet.com/v12/jspreadsheet.css" />
<link rel="stylesheet" href="https://jsuites.net/v6/jsuites.css" />
<script src="https://jspreadsheet.com/v12/jspreadsheet.js"></script>
<script src="https://jsuites.net/v6/jsuites.js"></script>

<div id="spreadsheet"></div>
<textarea id="agent-prompt" placeholder="Ask the agent to do something..."></textarea>
<button id="run-agent">Run Agent</button>
<div id="agent-log"></div>

<script>
// Pro licenses are issued per-domain. Start a free trial at jspreadsheet.com to get a key.
jspreadsheet.setLicense('YOUR_PRO_LICENSE_KEY');

const table = jspreadsheet(document.getElementById('spreadsheet'), {
    toolbar: true,
    tabs: true,
    worksheets: [{
        worksheetName: 'Sales',
        minDimensions: [6, 12],
        columns: [
            { title: 'Product', type: 'text', width: 140 },
            { title: 'Region', type: 'dropdown', source: ['North', 'South', 'East', 'West'], width: 100 },
            { title: 'Q1 Sales', type: 'numeric', mask: '$#,##0', width: 110, align: 'right' },
            { title: 'Q2 Sales', type: 'numeric', mask: '$#,##0', width: 110, align: 'right' },
            { title: 'Growth', type: 'text', width: 100, align: 'right' },
            { title: 'Notes', type: 'text', width: 200 },
        ],
        data: [
            ['Widget A', 'North', 45000, 52000, '', ''],
            ['Widget A', 'South', 38000, 41000, '', ''],
            ['Widget B', 'North', 67000, 63000, '', ''],
            ['Widget B', 'East', 29000, 35000, '', ''],
            ['Widget C', 'West', 51000, 48000, '', ''],
            ['Widget C', 'North', 72000, 81000, '', ''],
            ['Widget D', 'South', 19000, 22000, '', ''],
            ['Widget D', 'East', 44000, 39000, '', ''],
        ]
    }]
});
</script>

The toolbar: true and tabs: true flags are Pro-only — the toolbar gives users a visible undo trail of what the agent has done, and tabs let you chain agents across worksheets (a topic we'll come back to).

Step 2: Choose the tools

Next, you need to give the agent the set of operations it can run on your spreadsheet. Each tool is a function that takes structured parameters and modifies the spreadsheet according to the rules you set.

const worksheet = table[0];
const { getCellNameFromCoords } = jspreadsheet.helpers;

const tools = {
    read_data: {
        description: "Read all data from the spreadsheet including headers",
        execute: () => {
            const headers = worksheet.getHeaders().split(',');
            const data = worksheet.getData();
            return { headers, data, rowCount: data.length, colCount: headers.length };
        }
    },

    read_cell: {
        description: "Read the value of a specific cell. Pass raw=true to see the underlying formula instead of the computed value.",
        parameters: { col: "number", row: "number", raw: "boolean" },
        execute: ({ col, row, raw }) => worksheet.getValueFromCoords(col, row, false, raw === true)
    },

    write_cell: {
        description: "Write a literal value to a specific cell",
        parameters: { col: "number", row: "number", value: "string" },
        execute: ({ col, row, value }) => {
            worksheet.setValueFromCoords(col, row, value);
            return `Wrote "${value}" to cell (${col}, ${row})`;
        }
    },

    write_formula: {
        description: "Write a spreadsheet formula (=SUM, =AVERAGE, =IF, =VLOOKUP, =Sales!A1, etc.) to a cell. Jspreadsheet Pro's formula engine evaluates it live and recomputes when inputs change.",
        parameters: { col: "number", row: "number", formula: "string" },
        execute: ({ col, row, formula }) => {
            const expr = formula.startsWith('=') ? formula : `=${formula}`;
            worksheet.setValueFromCoords(col, row, expr);
            return `Wrote formula ${expr} to ${getCellNameFromCoords(col, row)}`;
        }
    },

    write_column: {
        description: "Batch-write values to an entire column starting from a given row",
        parameters: { col: "number", startRow: "number", values: "array" },
        execute: ({ col, startRow, values }) => {
            const batch = values.map((value, i) => ({ x: col, y: startRow + i, value }));
            worksheet.setValue(batch);
            return `Wrote ${values.length} values to column ${col}`;
        }
    },

    insert_row: {
        description: "Insert a new row at the end with given values",
        parameters: { values: "array" },
        execute: ({ values }) => {
            const y = worksheet.getData().length;
            const batch = values.map((value, x) => ({ x, y, value }));
            worksheet.setValue(batch);
            return `Inserted row at position ${y}`;
        }
    },

    set_style: {
        description: "Set the background color of a cell",
        parameters: { col: "number", row: "number", color: "string" },
        execute: ({ col, row, color }) => {
            const cell = getCellNameFromCoords(col, row);
            worksheet.setStyle(cell, 'background-color', color);
            return `Set background of ${cell} to ${color}`;
        }
    }
};

The agent can read the data, write literal values, write live formulas, batch-update columns, insert rows, and apply styles. That covers most of what Excel's Agent Mode gives you for basic analysis jobs, but you can do more if required: sorting, filtering, merging cells, setting validations. The pattern is the same.

write_formula is the one that earns Pro its keep. The agent can drop =SUM(C1:C8), =AVERAGE(D1:D8), or =IF(E1<0, "decline", "growth") straight into cells and the formula engine keeps them live — edit a Q1 value by hand, and the agent-written Growth formula recomputes on its own.

Step 3: Build the agent loop

Things get a little more interesting from here. The agent sends the available tools and the user's prompt to an LLM, executes the actions it wants, then folds the results back in. It keeps doing this until the model says it's done.

async function runAgent(userPrompt) {
    const log = document.getElementById('agent-log');
    log.textContent = 'Agent thinking...\n';

    // Build the tool descriptions for the LLM
    const toolDescriptions = Object.entries(tools).map(([name, tool]) => {
        let desc = `${name}: ${tool.description}`;
        if (tool.parameters) {
            desc += ` Parameters: ${JSON.stringify(tool.parameters)}`;
        }
        return desc;
    }).join('\n');

    // Read current spreadsheet state
    const currentData = tools.read_data.execute();

    const systemPrompt = `You are a spreadsheet agent. You have access to a spreadsheet with this data:

Headers: ${currentData.headers.join(', ')}
Data (${currentData.rowCount} rows):
${currentData.data.map((row, i) => `Row ${i}: ${row.join(', ')}`).join('\n')}

You can use these tools:
${toolDescriptions}

To use a tool, respond with a JSON block:
{"tool": "tool_name", "params": {...}}

You can call multiple tools in sequence. After each tool call, you'll see the result.
When you're done, respond with: {"done": true, "summary": "what you did"}

Think step by step. Read the data first if you need to understand it.`;

    let messages = [
        { role: 'system', content: systemPrompt },
        { role: 'user', content: userPrompt }
    ];

    // Agent loop: up to 10 steps
    for (let step = 0; step < 10; step++) {
        const response = await fetch('http://localhost:11434/api/chat', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
                model: 'llama3',
                messages: messages,
                stream: false
            })
        });

        const result = await response.json();
        const content = result.message.content;

        // Try to parse tool calls from the response
        const jsonMatch = content.match(/\{[\s\S]*?\}/);
        if (!jsonMatch) {
            log.textContent += `Agent: ${content}\n`;
            break;
        }

        let action;
        try {
            action = JSON.parse(jsonMatch[0]);
        } catch {
            log.textContent += `Agent: ${content}\n`;
            break;
        }

        // Check if agent is done
        if (action.done) {
            log.textContent += `Done: ${action.summary}\n`;
            break;
        }

        // Execute the tool
        if (action.tool && tools[action.tool]) {
            log.textContent += `Step ${step + 1}: ${action.tool}(${JSON.stringify(action.params || {})})\n`;
            const toolResult = tools[action.tool].execute(action.params || {});
            const resultStr = typeof toolResult === 'object' ?
                JSON.stringify(toolResult) : String(toolResult);

            log.textContent += `  Result: ${resultStr.substring(0, 200)}\n`;

            // Feed the result back so the agent can decide the next step
            messages.push({ role: 'assistant', content: content });
            messages.push({ role: 'user', content: `Tool result: ${resultStr}` });
        } else {
            log.textContent += `Unknown tool: ${action.tool}\n`;
            break;
        }
    }
}

document.getElementById('run-agent').addEventListener('click', () => {
    const prompt = document.getElementById('agent-prompt').value;
    if (prompt) runAgent(prompt);
});

That's basically the entire agent in about 80 lines. The user enters "calculate Q1 to Q2 growth for each product and highlight anything that declined," and the agent reads the data, works out the growth percentages, puts them into column E, and marks any negative values with a red background. Multiple steps, zero manual intervention.

What the agent can do

With just the seven tools above, the agent handles prompts like:

"Calculate the growth rate from Q1 to Q2 and flag declines." The agent reads the data, writes =(D1-C1)/C1 formulas into the Growth column so the calculation stays live, and highlights negative values in red.

"Which region has the highest total sales? Put a summary at the bottom." The agent reads all rows, sums Q1 and Q2 by region, and inserts a summary row with the winner.

"Add a notes column explaining why each product's trend matters." The agent reads the numbers, generates a brief analysis for each row using the LLM, and writes the text into the Notes column.

"Clean up this data: trim whitespace, standardize region names, fill in missing values." Data cleaning is where agents really earn their keep. The same task takes dozens of manual edits.

Making it production-ready

The tutorial code above works for a demo. Before you put it in front of real users, there are several important things to do.

Put a cap on the number of loops. The agent loop runs for up to 10 steps, but you should still add a timeout. A confused model can burn through steps without making any real progress. If three consecutive tool calls don't change the spreadsheet state, stop and ask the user to clarify.

Validate tool calls. The model can occasionally hallucinate tool names or pass the wrong parameters. Wrap every execute() in a try/catch block and return the error for self-correction. Most models will fix their own mistakes on the second attempt if you tell them what went wrong.

Queue writes. If the agent writes 50 cells in rapid succession, the spreadsheet will attempt to re-render after each one. Batch the writes: gather all the changes in the execution step, then apply them to the spreadsheet in a single pass.

Show progress. When AI runs silently, users get nervous about what's actually happening. Log each step visibly: "Reading data", "Calculating growth", "Writing results to column E", "Highlighting declines". That stops people from hitting the stop button.

For complex tasks, use a smarter model. Llama 3 8B deals with classification, math, and data writing well. But if you need multi-step reasoning across large datasets (50+ rows with complex conditional logic), you'll get better results from a 70B model or a cloud API like GPT-4o or Claude. The code stays the same — you're just swapping the model name and endpoint.

Cloud API option

If you want to use a cloud AI model (like Claude or GPT-4o) instead of running a model locally, you only need to change the part of the code that sends requests to the model. Everything else in your agent — tools and loop logic — stays the same.

// Replace the Ollama fetch with your preferred API
const response = await fetch('https://api.anthropic.com/v1/messages', {
    method: 'POST',
    headers: {
        'Content-Type': 'application/json',
        'x-api-key': YOUR_API_KEY,
        'anthropic-version': '2023-06-01'
    },
    body: JSON.stringify({
        model: 'claude-sonnet-4-6-20250514', // Check https://docs.anthropic.com/en/docs/about-claude/models for latest model IDs
        max_tokens: 1024,
        system: systemPrompt,
        messages: messages
    })
});

In production, it's a good idea to proxy this through your backend so the API key never touches the browser. The agent architecture doesn't care which model you use. Swap providers without rewriting anything on the spreadsheet side.

How this compares to the commercial agents

CapabilityExcel Agent ModeGoogle Sheets GeminiGPT for WorkYour Own Agent
Multi-step reasoningYesLimitedRow-by-rowYes
Data stays on your serverNoNoNoYes (with Ollama)
Model choiceGPT-5.2 or ClaudeGemini onlyGPT-4oAny model
Works in web appsNo (Excel only)No (Sheets only)Sheets/Excel add-inYes
Bulk row processingSlowLimited1,000 rows/minYour hardware is the limit
Custom toolsNoNoNoYes
Own the sourceNoNoNoYes (your agent code on your infra)
CostMicrosoft 365 + CopilotWorkspaceSubscriptionJspreadsheet Pro license + your compute

The commercial agents are polished, and they work out of the box. But what they don't give you is control. You can't add a custom tool that calls your internal API, swap the model when a better one drops, or keep the data on your own infrastructure. Sure, building your own takes a bit more work upfront, but the key thing is that you end up owning every piece of it.

Going further

Once the basic agent loop works, there are a few directions to take it:

Ingest XLSX. Register @jspreadsheet/parser and give the agent a load_xlsx tool. Users upload a workbook, the parser hands the agent a worksheet config, and the agent analyzes it like any other sheet. Round-tripping real Excel files is where most internal-tooling requirements actually live.

Add validation tools. Pro's validation rules (required fields, ranges, patterns) give the agent a way to enforce data quality as it writes — drop a set_validation tool next to set_style and the agent can refuse bad inputs before they land.

Implement MCP. The Model Context Protocol is how AI coding assistants like Claude Code and Cursor connect to external tools. Building an MCP server for your spreadsheet means any MCP-compatible AI assistant can read and write your spreadsheet data. The agent pattern in this tutorial is a stepping stone toward that.

Chain agents across worksheets. With tabs: true on, one agent analyzes the Sales sheet, a second agent builds a Summary sheet with pivot-style formulas referencing =Sales!A1, a third agent emails the report. Cross-sheet references are Pro-only and turn the workbook into a workflow engine.

Getting started

  1. Start a free Jspreadsheet Pro trial to get a license key
  2. Install Ollama and pull a model: ollama pull llama3
  3. Copy the Step 1 scaffold, paste your license key into jspreadsheet.setLicense(...), and load the page
  4. Paste in the tools + agent loop from Steps 2 and 3
  5. Start with a simple prompt like "calculate Q1 to Q2 growth and flag declines" and watch the agent work
  6. Add tools as you need them: formulas, validations, XLSX import, API calls

The full code from this tutorial works in any modern browser. No build step, no framework required — just the Pro CDN bundle, a license key, and any LLM endpoint you like.

Related