SafeXL Excel Spreadsheet Editor for AI Agents
Pricing
Pay per event
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.
Pricing
Pay per event
Rating
0.0
(0)
Developer
D. Michael Piscitelli
Maintained by CommunityActor stats
1
Bookmarked
2
Total users
1
Monthly active users
3 days ago
Last modified
Categories
Share
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 forokrows,nullotherwise.error:{type, message, retryable}, ornull.typeis a fixed list of codes.retryable: whether the same entry may succeed on a later run.
Example 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\"}"]}]}
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:
{"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 inresult.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
timeouterror, 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 asfile_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,verify9 to 12 s. 300,000 rows: set values 10 to 15 s,verify23 to 27 s. On build 0.1.14, single runs of set values at 300,000 rows, insert at 100,000 andverifyat 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, onlyviewwas 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 asfile_too_complex, not charged. Retrying afile_too_complexrefusal doesn't help. - Damaged inside (a corrupt part, a bad checksum): refused as
not_a_workbook. A symbolic-link entry: refused asunsafe_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.