# SafeXL Excel Spreadsheet Editor for AI Agents (`herakles-dev/safexl-workbook`) Actor

Edits Excel .xlsx and .xlsm spreadsheets for AI agents with no code sandbox. The receipt checks that macros, charts and pivots are still there. Also verifies a file against its original, rescues one a model re-saved, and shows a compact view.

- **URL**: https://apify.com/herakles-dev/safexl-workbook.md
- **Developed by:** [D. Michael Piscitelli](https://apify.com/herakles-dev) (community)
- **Categories:** Developer tools, Automation, AI
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 1 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per event

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## SafeXL Excel Spreadsheet Editor for AI Agents

Edits Excel .xlsx and .xlsm spreadsheets for AI agents with no code sandbox. The receipt checks that macros, charts and pivots are still there. Also verifies a file against its original, rescues one a model re-saved, and shows a compact view.

### What it does

Edits .xlsx and .xlsm files for agents that can't run code. Send the file and the changes, get the file back with a receipt.

Why it exists: a model that edits a workbook by writing code re-saves the whole file through a library. Parts it never touched can go missing. Macros gone, charts orphaned.

Doesn't: delete rows, insert columns, make charts, recalculate. Refuses worksheets written with a namespace prefix (some files from the .NET OpenXML SDK). Tested in LibreOffice, not Excel.

The Actor does no AI work: it applies your operations directly to the file, then checks its own result against your original. A file is delivered only when that check passes. It does four jobs. `edit` applies an ordered list of operations and delivers the new file. `verify` compares an edited file with its original and returns the receipt. `salvage` takes a copy that another tool re-saved and lost features, re-applies its cell edits to the original and delivers the rebuilt file. `view` returns a compact text view of a sheet, so you do not have to read raw XML.

### What the receipt covers

Covers: loss of a tracked feature (macros, charts, pivots, validation, defined names and the rest), and untouched parts byte-identical to your original.

Doesn't cover: whether the cell values or formulas are the ones you meant. Check the cells you changed. The receipt lists defined names and table ranges that changed, under `reference_changes`. It can't tell a wanted change from an unwanted one. That list covers names and tables only. A data validation that grew or moved down, or a chart series whose rows changed, is not listed.

`verify` runs the same check on any two files you send. It finds lost features and parts that changed when they shouldn't. It is not a tamper check on cell values.

Formulas are stored as written. The Actor does not evaluate or sanitise them, including `WEBSERVICE`, DDE and `HYPERLINK` formulas. Whether they run is up to the application that opens the file. Check formulas from an untrusted source before you send them.

Risky additions don't block an edit. Add a `WEBSERVICE` formula or hide a sheet and you still get the file, and pay for it. You asked for it. The receipt names each one under `added_risk`.

Also in the receipt: `summary`, one sentence. `cell_changes`, counts per sheet. `reference_changes`, each defined name or table range that differs.

### Operations

Each `ops` entry: one JSON object written as text, with an `op` field. Cells are single cells such as `B2`. A bad entry is refused with its index and reason. Nothing is written or charged.

| op | Fields | Example 1 | Example 2 |
|---|---|---|---|
| `set_value` | sheet, cell, value (number, true/false or text) | `{"op": "set_value", "sheet": "Sales", "cell": "B2", "value": 120}` | `{"op": "set_value", "sheet": "Sales", "cell": "A7", "value": "East"}` |
| `set_formula` | sheet, cell, formula | `{"op": "set_formula", "sheet": "Sales", "cell": "E2", "formula": "=B2*C2"}` | `{"op": "set_formula", "sheet": "Summary", "cell": "B3", "formula": "=AVERAGE(Sales!C2:C6)"}` |
| `set_text` | sheet, cell, text (never turned into a number) | `{"op": "set_text", "sheet": "Items", "cell": "A5", "text": "007"}` | `{"op": "set_text", "sheet": "Sales", "cell": "G1", "text": "Reviewed"}` |
| `insert_rows` | sheet, row, count (default 1) | `{"op": "insert_rows", "sheet": "Sales", "row": 3}` | `{"op": "insert_rows", "sheet": "Sales", "row": 2, "count": 4}` |
| `insert_cells` | sheet, range, shift (down) | `{"op": "insert_cells", "sheet": "Sales", "range": "B5:C6", "shift": "down"}` | `{"op": "insert_cells", "sheet": "Items", "range": "D2:D2"}` |
| `rename_sheet` | sheet, new_name | `{"op": "rename_sheet", "sheet": "Summary", "new_name": "Totals"}` | `{"op": "rename_sheet", "sheet": "Sheet1", "new_name": "Q3 Data"}` |
| `add_sheet` | name | `{"op": "add_sheet", "name": "Notes"}` | `{"op": "add_sheet", "name": "Checks"}` |
| `resize_table` | table, ref, sheet (optional) | `{"op": "resize_table", "table": "SalesTbl", "ref": "A1:E8"}` | `{"op": "resize_table", "table": "ItemsTbl", "ref": "A1:D10", "sheet": "Items"}` |

Formulas: stored as written, calculated when Excel or LibreOffice next opens the file. The Actor doesn't calculate them. Files come from an https URL to a public file, a record key in a key-value store you pick in the input, or `sample://sales` and `sample://inventory` (two small workbooks to try it on).

### For AI agents

In: one JSON object. Out: one dataset row per entry. Every row has the same fields:

- `input_index`: position of the entry in your input list, from 0. Rows come back in input order.
- `outcome`: `ok`, or one of four that are never charged: `empty` (nothing found), `out_of_scope` (refused), `error`, `skipped` (a limit was reached).
- `units_charged`: events this entry cost you.
- `result`: the typed result for `ok` rows, `null` otherwise.
- `error`: `{type, message, retryable}`, or `null`. `type` is a fixed list of codes. `retryable`: whether the same entry may succeed on a later run.

Example input:

```json
{
  "jobs": [
    {
      "operation": "view",
      "workbook": "sample://sales",
      "sheet": "Sales",
      "max_cells": 40
    },
    {
      "operation": "edit",
      "workbook": "sample://sales",
      "ops": [
        "{\"op\": \"set_value\", \"sheet\": \"Sales\", \"cell\": \"B2\", \"value\": 120}",
        "{\"op\": \"set_formula\", \"sheet\": \"Sales\", \"cell\": \"E2\", \"formula\": \"=B2*C2\"}"
      ]
    }
  ]
}
```

One real row, from the second job of the example above (an edit), build 0.1.11. The store id and the record key are masked:

```json
{
  "input_index": 1,
  "source": "edit",
  "outcome": "ok",
  "units_charged": 1,
  "result": {
    "operation": "edit",
    "engine": "safexl 0.2.0",
    "input_sha256": "56769aa760e66abc37afda7be543bf040206bfeaf85248e11c72af58a9c02d38",
    "receipt": {
      "verdict": "intact",
      "valid": true,
      "lost_features": [],
      "rebuilt": false,
      "parts": {
        "unchanged": 14,
        "changed": [
          "xl/worksheets/sheet1.xml"
        ],
        "added": [],
        "removed": []
      },
      "before_sha256": "56769aa760e66abc37afda7be543bf040206bfeaf85248e11c72af58a9c02d38",
      "after_sha256": "298a37672377766ef9a726a7d5ee5193a8376fa198d0a356f461e510f35b6605",
      "reasons": [],
      "added_risk": [],
      "cell_changes": {
        "Sales": {
          "values_changed": 2,
          "formulas_changed": 0,
          "styles_changed": 0,
          "added": 0,
          "removed": 0,
          "rows_removed": 0,
          "cells_changed": 2,
          "complete": true
        }
      },
      "summary": "No tracked feature was lost and untouched parts are byte-identical; 2 cells changed on Sales; nothing risky was added."
    },
    "output_sha256": "298a37672377766ef9a726a7d5ee5193a8376fa198d0a356f461e510f35b6605",
    "output_bytes": 7857,
    "macro_enabled": false,
    "ops_applied": 2,
    "output": {
      "key": "<record key of the edited file>",
      "store_id": "<store id>",
      "url": "https://api.apify.com/v2/key-value-stores/<store id>/records/job-1-edit.xlsx"
    }
  },
  "notices": [],
  "error": null,
  "cost_usd": 0,
  "latency_s": 0.823
}
```

Edited file: the record named in `result.output.key`, in the run's default key-value store. Its sha256 is `result.output_sha256`. `receipt.verdict` is `intact` whenever a file is delivered.

`out_of_scope`: the Actor refused the job. `error.type` says why (for example `invalid_ops`, `edit_refused`, `not_intact`, `unsafe_workbook`, `url_not_allowed`, `record_not_found`). A refusal is never charged.

### Input

- `jobs`: Each job is one operation on one workbook: edit, verify, salvage or view. Up to 5 entries per run, each up to 400000 characters.
  Each entry is a JSON object with these fields:
  - `operation` (string, required): Operation. edit applies the ops and delivers the edited file with a receipt. verify compares workbook against original and returns a receipt. salvage re-applies the cell edits found in a damaged copy (workbook) to the original and delivers the rebuilt file. view returns a compact text view of the workbook.
  - `workbook` (string, required): Workbook. The file to act on: an https URL to a public .xlsx or .xlsm, a record key in the key-value store picked in 'Key-value store with your workbooks', or sample://sales or sample://inventory for a small bundled workbook. For verify this is the edited file; for salvage it is the damaged copy.
  - `original` (string, optional): Original workbook. Required for verify and salvage: the workbook before the edit, in the same forms as 'workbook'.
  - `ops` (array, optional): Operations (edit). Required for edit. An ordered list, 1 to 40, each a JSON object written as text with an "op" field: set_value {sheet, cell, value}, set_formula {sheet, cell, formula}, set_text {sheet, cell, text}, insert_rows {sheet, row, count}, insert_cells {sheet, range, shift: down}, rename_sheet {sheet, new_name}, add_sheet {name}, resize_table {table, ref, sheet}.
  - `extend_tables` (boolean, optional): Extend tables (edit). When true (the default), a cell written directly beside an Excel table grows the table to include it, as Excel does.
  - `sheet` (string, optional): Sheet (view). view only: the sheet to show. The first sheet when empty; all sheets when 'find' is set and this is empty.
  - `range` (string, optional): Range (view). view only: an A1 cell or range such as A1:F40 to limit the view.
  - `find` (string, optional): Find (view). view only: list cells whose value or formula contains this text, case-insensitive.
  - `max_cells` (integer, optional): Max cells (view). view only: the most cells to return, 1 to 2000. The result says when it was cut off and where to continue.

### Output

One dataset row per entry, in input order. The Overview tab shows the same rows as a table.

### Pricing

You pay per delivered result, and only for results the Actor delivered:

- $0.05 per edited workbook (`workbook-edit`): the edited file, with a receipt whose verdict is intact.
- $0.10 per salvaged workbook (`workbook-salvage`): the rebuilt file, with an intact receipt. A salvage can be partial: the edits it could not carry over are listed in `result.salvage.skipped`, and it is still charged. Check that list.
- $0.01 per receipt (`workbook-verify`).
- $0.002 per view (`workbook-view`).

Apify's standard start fee applies once per run: $0.00005.

Refused jobs, errors, timeouts and skipped jobs are never charged. The start fee applies to every run, even one where every job is refused. Runs on the free Apify plan handle up to 3 results per run.

### Limits

- Entries: up to 5 per run, each up to 400000 characters.
- Time: 60 s per entry. A longer one is stopped, returned as a `timeout` error, not charged. A job usually takes a few seconds.
- Files: .xlsx and .xlsm up to 20 MB. Refused: password-protected files, legacy .xls, files with an XML DOCTYPE. The Actor never opens or runs a macro. A macro project is copied byte for byte.
- URLs: https only, public hosts only, at most 3 redirects, 20 seconds.
- Key-value records: loaded whole before the size check. A record far over 20 MB can use up the 1 GB of memory and fail the run. Keep workbooks under 20 MB.
- Size: row limits (rows stored in the file, including a row that holds only formatting), checked before the work starts where the Actor can read the sheet list. Cell edits (set value, formula, text): 300,000 rows in the sheet edited. Row and cell inserts: 100,000 rows in that sheet. `verify`: 300,000 rows across all sheets of each of the two files. Over a limit: refused as `file_too_complex`, not charged (the start fee still applies). Each limit is the largest size that finished in every run on Apify at 1 GB on 4 October 2026 (build 0.1.11), sheets with one number per row, four runs each. 100,000 rows: set values 4 to 6 s, insert 1 row 12 to 24 s, `verify` 9 to 12 s. 300,000 rows: set values 10 to 15 s, `verify` 23 to 27 s. On build 0.1.14, single runs of set values at 300,000 rows, insert at 100,000 and `verify` at 300,000 fell inside or under those ranges. Measured on set value and on inserting one row; formula, text and cell inserts use the same limits unmeasured. Several inserts in one entry take longer than one. A denser workbook is slower. An edit re-checks the whole file, so other large sheets in the workbook add time. `view`, `salvage`, rename, add sheet and resize table are not row-checked. Of these, only `view` was measured on a large sheet (one run, 700,000 rows, about 10 s). The others can still time out on a very large workbook.
- Too slow: stopped, returned as `timeout`, not charged. Too big for memory: refused as `file_too_complex`, not charged. Retrying a `file_too_complex` refusal doesn't help.
- Damaged inside (a corrupt part, a bad checksum): refused as `not_a_workbook`. A symbolic-link entry: refused as `unsafe_workbook`.
- Operations: at most 40 per edit. No formula calculation, no clearing cells, no deleting rows, no chart or pivot edits.

### Contact

Issues tab on the Actor's page: wrong results and questions. I read it and reply as I can.

# Actor input Schema

## `jobs` (type: `array`):

Each job is one operation on one workbook: edit, verify, salvage or view.

## `workbookStore` (type: `string`):

Only needed when a job names a record key instead of an https URL or a sample. Pick a key-value store you own; the Actor gets read access to just that store. Leave empty otherwise.

## Actor input object example

```json
{
  "jobs": [
    {
      "operation": "view",
      "workbook": "sample://sales",
      "sheet": "Sales",
      "max_cells": 40
    },
    {
      "operation": "edit",
      "workbook": "sample://sales",
      "ops": [
        "{\"op\": \"set_value\", \"sheet\": \"Sales\", \"cell\": \"B2\", \"value\": 120}",
        "{\"op\": \"set_formula\", \"sheet\": \"Sales\", \"cell\": \"E2\", \"formula\": \"=B2*C2\"}"
      ]
    }
  ]
}
```

# Actor output Schema

## `results` (type: `string`):

One dataset item per job: its outcome (ok, out_of_scope, error, skipped), units charged, the typed result with its receipt, notices and any error.

## `overview` (type: `string`):

The same items as a table: one row per entry with its outcome and charge.

## `files` (type: `string`):

The edited and salvaged files, one record per delivered job (job-<position>-<operation>.xlsx), in the run's default key-value store.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "jobs": [
        {
            "operation": "view",
            "workbook": "sample://sales",
            "sheet": "Sales",
            "max_cells": 40
        },
        {
            "operation": "edit",
            "workbook": "sample://sales",
            "ops": [
                "{\"op\": \"set_value\", \"sheet\": \"Sales\", \"cell\": \"B2\", \"value\": 120}",
                "{\"op\": \"set_formula\", \"sheet\": \"Sales\", \"cell\": \"E2\", \"formula\": \"=B2*C2\"}"
            ]
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("herakles-dev/safexl-workbook").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "jobs": [
        {
            "operation": "view",
            "workbook": "sample://sales",
            "sheet": "Sales",
            "max_cells": 40,
        },
        {
            "operation": "edit",
            "workbook": "sample://sales",
            "ops": [
                "{\"op\": \"set_value\", \"sheet\": \"Sales\", \"cell\": \"B2\", \"value\": 120}",
                "{\"op\": \"set_formula\", \"sheet\": \"Sales\", \"cell\": \"E2\", \"formula\": \"=B2*C2\"}",
            ],
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("herakles-dev/safexl-workbook").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "jobs": [
    {
      "operation": "view",
      "workbook": "sample://sales",
      "sheet": "Sales",
      "max_cells": 40
    },
    {
      "operation": "edit",
      "workbook": "sample://sales",
      "ops": [
        "{\\"op\\": \\"set_value\\", \\"sheet\\": \\"Sales\\", \\"cell\\": \\"B2\\", \\"value\\": 120}",
        "{\\"op\\": \\"set_formula\\", \\"sheet\\": \\"Sales\\", \\"cell\\": \\"E2\\", \\"formula\\": \\"=B2*C2\\"}"
      ]
    }
  ]
}' |
apify call herakles-dev/safexl-workbook --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,herakles-dev/safexl-workbook"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/UDx08fk75J6Nyg18O/builds/BzkE7JnG8u2jqT5U2/openapi.json
