Use this skill when a user requests to create, modify, analyze, visualize, or work with spreadsheet files (`.xlsx`, `.xls`, `.csv`, `.tsv`) with formulas, formatting, charts, tables, and recalculation.
This skill includes guidelines for how to produce a correct, polished spreadsheet artifact quickly that completes the user's request. When producing spreadsheets or workbooks, you will be judged on layout, readability, style, adherence to industry norms/conventions and correctness. Follow the guidelines below for how to use the APIs effectively and how to verify your output before finalizing work for the user.
For complex, analytical, financial or research involved tasks, you are especially judged on correctness and quality. You need to be professional. For these, always make sure you have a plan for how you're organizing the spreadsheet, and the data or visualizations within each sheet. For business, finance, operations, dashboard, and data-analysis prompts, aim for an output that can compete with a strong analyst-built workbook, not just a functional grid. A good default shape is an executive summary or dashboard first, then source/assumptions, then model/detail sheets. For simpler tasks like a creating template or tracker, or things that do not require research, prioritize doing the spreadsheet build and edits quickly, while ensuring the user's request is fulfilled.
For additional stylistic best practices, follow: style_guidelines.md
@oai/artifact-tool JS package which exists in the default Codex workspace dependencies node_modules for authoring, editing, inspecting, rendering, and exporting spreadsheet .xlsx workbooks. Use the bundled workspace Node.js and Python runtimes for local builders and helper scripts.NODE_PATH; ensure bundled packages resolve through normal Node package lookup from the builder file.node_modules directory link or Windows junction to the bundled node_modules; do not copy bundled dependency directories or import internal package files directly..xlsx artifact(s), with this link format [Revenue Model - model.xlsx](/absolute/path/to/model.xlsx); using a platform-appropriate absolute filesystem path. If there are multiple requested final workbooks, put each final .xlsx Markdown link on its own line. Do not wrap final artifact links in backticks or code fences, and do not put them in bullets, headings, or prose sentences.artifact_tool, artifact-tool, @oai/artifact-tool, the Node/JS builder, copied builder scripts, package manifests, export workflow, verification workflow, rendered previews, or internal tooling unless explicitly requested.openpyxl, xlsxwriter, or pandas.ExcelWriter unless the user explicitly asks for a non-artifact-tool fallback.pandas/numpy are available in the bundled Python runtime in the default Codex workspace dependencies. Save analysis outputs as JSON/CSV; the JavaScript builder should read those files and create the workbook with @oai/artifact-tool. Keep workbook-derived or user-editable calculations as spreadsheet formulas when auditability or future editing matters.SKILL.md from disk. Move directly to the prompt, attachments, and workbook build.@oai/artifact-tool, create workbook/sheets for new files..xlsxIf a user asks to edit or add to an existing spreadsheet:
On first error:
workbook.help("<exact_api>") query only if needed.Do not loop indefinitely on similar failures.
autofit + wrap_text=H6*(1+$B$3))."", 0, or a clear "No entries yet" state as appropriate. Alternatively, prefill with a few rows of example data.Complete only when:
.xlsx saved to outputs/<unique_thread_id>/.Before final response, verify values/formulas and visual quality.
const check = await workbook.inspect({
kind: "table",
range: "Dashboard!A1:H20",
include: "values,formulas",
tableMaxRows: 20,
tableMaxCols: 12,
});
console.log(check.ndjson);
Inspect targeting:
"Sheet!A1:H20") or sheetId.const errors = await workbook.inspect({
kind: "match",
searchTerm: "#REF!|#DIV/0!|#VALUE!|#NAME\\?|#N/A",
options: { useRegex: true, maxResults: 300 },
summary: "final formula error scan",
});
console.log(errors.ndjson);
const blob = await workbook.render({ sheetName: "Sheet1", range: "A1:H20", scale: 2 });
Make sure you do at least one visual pass of all the sheets in the workbook before the final export.
Visual requirements:
await fs.mkdir(outputDir, { recursive: true });
const output = await SpreadsheetFile.exportXlsx(workbook);
await output.save(`${outputDir}/output.xlsx`);
.xlsx variants unless asked.Read the following templates instructions ONLY when a request relates to any of the following:
Do not load these for other tasks that are unrelated unless the prompt explicitly asks for it.
pypdf, if available, then use one small structured extraction script to collect all required facts into a dict/JSON object. Avoid many ad hoc rg/sed passes over the same text.Import existing workbook only when needed:
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
const input = await FileBlob.load("path/to/input.xlsx");
const workbook = await SpreadsheetFile.importXlsx(input);
Import CSV text directly when the source or intermediate data is CSV:
import fs from "node:fs/promises";
import { Workbook } from "@oai/artifact-tool";
const csvText = await fs.readFile("path/to/input.csv", "utf8");
const workbook = await Workbook.fromCSV(csvText, { sheetName: "Sheet1" });
Prefer Workbook.fromCSV(...) over hand-parsing CSV rows; clean or analyze CSV with Python/Node first only when needed.
Create new workbook:
import fs from "node:fs/promises";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
const workbook = Workbook.create();
const sheet = workbook.worksheets.add("Inputs");
Final export:
await fs.mkdir(outputDir, { recursive: true });
const output = await SpreadsheetFile.exportXlsx(workbook);
await output.save(`${outputDir}/output.xlsx`);
range.values, range.formulas) over per-cell loops. Matrix shape must match the target range (for example "D4:M4" should be a 1x10 matrix, row x col).fillDown() / fillRight(). For dynamic-array formulas (SEQUENCE, UNIQUE, FILTER, SORT, VSTACK, HSTACK), write only the anchor cell and let the result spill after.range.displayFormulas plus range.formulaInfos when you need to understand a spill child or a data-table output cell.Date objects for sortable/charted/formula date columns.yyyy-mm-dd).string | number | boolean | null.=, write it as a value prefixed with a single quote (for example '=B2*C2). This includes formula descriptions, validation examples, and labels; do not write these cells through range.formulas.sheet.deleteAllDrawings(). range.clear() does not remove charts, shapes, or images.await workbook.inspect(...); use workbook.help(...) only when the quick surface below is insufficient.sheet.getRange("A1:C10")).sheet.charts, sheet.shapes, sheet.images): 0-based { row, col }.rowOffsetPx, colOffsetPx, widthPx, heightPx).workbook.help(...) only when blocked by uncertainty.chart, worksheet.getRange, worksheet.freezePanes, range.dataValidation, chart.series.add). If an exact path fails, one broader wildcard search is allowed.render can be used to examine an existing workbook visually and for visual verifications.Useful help calls:
console.log(workbook.help("shape.add", { include: "examples,notes" }).ndjson);
console.log(
workbook.help("*", {
search: "fill|borders|autofit",
include: "index,examples,notes",
maxChars: 6000,
}).ndjson,
);
inspect to understand what already exists and where.inspect(...) for workbook understanding and discovery across broad areas.range.formulas when you already know the target range and need the exact rectangular formula matrix.inspect({ kind: "formula", ... }) over reading range.formulas across a very large area.maxChars, tableMaxRows, tableMaxCols, and/or maxResults to prevent large dumps of data.await wb.inspect({
kind: "workbook,sheet,table",
maxChars: 6000,
tableMaxRows: 6,
tableMaxCols: 6,
tableMaxCellChars: 80,
});
await wb.inspect({ kind: "sheet", include: "id,name" })await wb.inspect({ kind: "formula", sheetId: firstSheetName, range: "A1:Z30", maxChars: 2500, options: {maxResults:50} })await wb.inspect({ kind: "computedStyle", sheetId: firstSheetName, range: "A1:E10", maxChars: 2500 })kind tokens: workbook, sheet, table, region, match, formula, thread, computedStyle, definedName, drawingawait wb.inspect({
kind: "region",
sheetId: firstSheetName,
range: "A1:Z30",
maxChars: 2500,
});
"id" values (for example "ws/r5qsk5"), which you can resolve back to workbook objects with wb.resolve(...):wb.resolve("ws/...") -> worksheetwb.resolve("th/...") -> comment threadconst range = sheet.getRange("I23:N24");
range.merge();
range.values = [["Some long description that should cross multiple cells"]];
Workbook.create() starts with no sheets; add one before calling getActiveWorksheet().A:A, $A:$A, or Sheet!B:B. Prefer bounded ranges sized to the editable table, e.g. $A$6:$A$205, especially inside COUNTIFS, SUMIFS, INDEX, and lookup formulas.invalid int32: NaN or another serialization error after formatting or charts, do not rerun the same script unchanged. First remove or simplify optional drawing/chart/style details in this order: nested/custom border configs, custom chart axis/style mutations, broad row/column formatting/autofit calls, then nonessential drawings. Preserve values/formulas and keep or retry a minimal helper-range chart when the prompt needs charts; only drop all charts/drawings if the simplified chart still prevents export.inside/around/outline border configs and border weight settings unless you have already checkpoint-exported that exact pattern successfully.import { FileBlob, SpreadsheetFile, Workbook } from "@oai/artifact-tool"const workbook = Workbook.create(); const sheet = workbook.worksheets.add("Sheet1")const workbook = await SpreadsheetFile.importXlsx(arrayBufferOrFileBlob)const xlsx = await SpreadsheetFile.exportXlsx(workbook); await xlsx.save("output.xlsx")const inspect = await workbook.inspect({ kind: "sheet", include: "id,name", sheetId, range: "A1:C10" })const help = workbook.help("worksheet.getRange", { include: "index,examples" })const blob = await workbook.render({ sheetName: "Sheet1", autoCrop: "all", scale: 1, format: "png" })const previewBytes = new Uint8Array(await preview.arrayBuffer());
await fs.writeFile(`${outputDir}/preview.png`, previewBytes);
const workbook = await Workbook.fromCSV(csvText, { sheetName: "Sheet1" })await workbook.fromCSV(csvText, { sheetName: "ImportedData" })workbook.worksheets.add(name)workbook.worksheets.getItem(name)workbook.worksheets.getOrAdd(name, { renameFirstIfOnlyNewSpreadsheet: true })workbook.worksheets.getItemAt(index)workbook.worksheets.getActiveWorksheet() (only after at least one sheet exists)sheet.getRange("A1:C10"), sheet.getRangeByIndexes(startRow, startCol, rowCount, colCount), sheet.getCell(row, col)sheet.getUsedRange(valuesOnly?)sheet.mergeCells("A1:C1"), sheet.unmergeCells("A1:C1")sheet.freezePanes.freezeRows(1), sheet.freezePanes.freezeColumns(2), sheet.freezePanes.unfreeze()sheet.tables, sheet.charts, sheet.sparklineGroups (sheet.sparklines alias), sheet.shapes, sheet.imagessheet.showGridLines = falsesheet.dataTables, sheet.conditionalFormattings, sheet.dataValidationssheet.deleteAllDrawings() removes charts, shapes, and images before a dashboard rebuild.const range = sheet.getRange("A1:C10")range.values = [[...], ...] (2D matrix of values)range.formulas = [["=..."], ...]range.formulasR1C1 = [["=RC[-1]*2"]]range.values / range.formulas / range.displayFormulas / range.formulaInfos (for spill/array formulas)range.write(matrixOrPayload) (auto-sizes/spills from anchor as needed)range.writeValues(matrixOrRows)range.fillDown(), range.fillRight()
sheet.getRange("D2").formulas = [["=..."]]sheet.getRange("D2:D200").fillDown()range.clear({ applyTo: "contents" | "formats" | "all" })range.copyFrom(sourceRange, "values" | "formulas" | "all") source and destination must have the same shaperange.copyTo(destRange, "values" | "formulas" | "all")range.offset(...), range.resize(...), range.getCurrentRegion(), range.getRow(i), range.getColumn(j)range.getRangeByIndexes(...), range.getCell(...)range.merge(), range.merge(true) to merge across, range.unmerge()range.format supports fill, font, numberFormat, borders, alignments, wrapTextrange.format.autofitColumns(), range.format.autofitRows()range.format.columnWidth = 18, range.format.rowHeight = 24range.format.columnWidthPx = 120, range.format.rowHeightPx = 24range.setNumberFormat("yyyy-mm-dd")range.format.numberFormat = [["0"], ["0.00"], ["@"]]range.dataValidation = { rule: { type: "list", formula1: "Categories!$A$2:$A$4" } }range.dataValidation = { rule: { type: "list", values: ["Not Started", "In Progress"] } }sheet.dataValidations.add({ range: "B2:B100", rule: { type: "whole", operator: "between", formula1: 1, formula2: 10 } })range.conditionalFormats.add(type, config)range.conditionalFormats.addCustom(formula, format)range.conditionalFormats.addCellIs({...})range.conditionalFormats.addDataBar({...})range.conditionalFormats.addColorScale({...})range.conditionalFormats.deleteAll() / range.conditionalFormats.clear()sheet.conditionalFormattings.add({ range, rule })const r = sheet.getRange("B2:B20");
r.conditionalFormats.addCellIs({
operator: "lessThan",
formula: 0,
format: { font: { color: "#DC2626" } },
});
r.conditionalFormats.addCustom("=B2<0", { fill: "#FECACA" });
r.conditionalFormats.addColorScale({
minColor: "#FEE2E2",
midColor: "#FEF3C7",
maxColor: "#DCFCE7",
});
r.conditionalFormats.addDataBar({ color: "accent5", gradient: true });
TasksTable, SummaryTable).inspect summary over a separate tables-only scan when available.const table = sheet.tables.add("A1:H200", true, "TasksTable")table.rows.add(null, [[...], ...]), table.getDataRows(), table.getHeaderRowRange()sheet.tables.items -> Table[]table.name, table.style, table.style, table.showHeaderstable.showTotals, table.showBandedColumns = true, table.showFilterButtontable.delete()sheet.getRange("F4:H7").values = [
["Month", "Revenue", "EBITDA"],
["Jan", 100, 10],
["Feb", 120, 18],
["Mar", 130, 22],
];
const chart = sheet.charts.add("line", sheet.getRange("F4:H7"));
chart.setPosition("J4", "Q20");
chart.title = "Revenue and EBITDA Trend";
chart.hasLegend = true;
chart.xAxis = { axisType: "textAxis" };
chart.yAxis = { numberFormatCode: "$#,##0" };
const chart = sheet.charts.add("line", sourceRange) when the source range already has headers and text x-axis labels.chart.series.add(...) and chart.legend = {...} on the first pass, unless source-range based chart creation does not work (for example, non-continuous data). Use a helper range chart first, then add optional chart styling only if the basic chart renders and exports cleanly.const chart = sheet.charts.add("bar", chartProps), then checkpoint export before adding optional styling.chart.setPosition("F2", "M20").sheet.charts.getItemOrNullObject("Chart 1"), sheet.charts.deleteAll()chart.xAxis = { axisType: "textAxis", tickLabelInterval: 2 } and chart.yAxis = { numberFormatCode: "$#,##0" }. These help legibility and visibility.Jan 2025 or 2025-01. Do not rely on date axis number formats alone; rendered previews can show Excel serial numbers."bar" | "line" | "area" | "pie" | "doughnut" | "scatter" | "bubble" | "radar" | "stock" | "treemap" | "sunburst" | "histogram" | "boxWhisker" | "waterfall" | "funnel" | "map".sheet.sparklines.add({...}), sheet.sparklines.clear(), sheet.sparklines.deleteAll() (sheet.sparklineGroups is the worksheet-scoped alias)Use workbook.help(...) primarily for obscure/advanced surfaces (for example deep chart axis settings, unusual drawing configs, pivot APIs, or uncommon option schemas).
workbook.help("enum.ShapeGeometry", { include: "index,notes" }).ndjsonworkbook.help("enum.*", { search: "ShapeGeometry|LineStyle", include: "index" }).ndjsonworkbook.help("shape.add", { include: "examples,notes" }).ndjsonworkbook.help("*", { search: "fill|borders|autofit", include: "index,examples,notes", maxChars: 6000 }).ndjsonimport fs from "node:fs/promises";
import { SpreadsheetFile, Workbook } from "@oai/artifact-tool";
async function quickApiExample() {
const workbook = Workbook.create();
var sheet = workbook.worksheets.add("ExampleSheet");
sheet = workbook.worksheets.getItem("ExampleSheet");
sheet.getRange("A1:D4").values = [
["Name", "Personality Type", "Age", "Birthday"],
["John Doe", "Introvert", 30, new Date("1990-01-01T00:00:00Z")],
["Jane Smith", "Extrovert", 25, new Date("1995-02-15T00:00:00Z")],
["Jim Very Long Name", "Ambivert", 40, new Date("1980-03-20T00:00:00Z")],
];
sheet.getRange("E1").values = [["Score"]];
sheet.getRange("E2").formulas = [["=C2*10"]]; // score is 10 * age
sheet.getRange("E2:E10").fillDown();
const headerRange = sheet.getRange("A1:E1");
// Styling
const headerFormats = {
fill: "#0F766E",
font: { bold: true, color: "#FFFFFF" },
horizontalAlignment: "center",
verticalAlignment: "center",
rowHeight: 16,
};
headerRange.format = headerFormats;
headerRange.format.autofitColumns();
const dataRange = sheet.getRange("A2:D10");
dataRange.format.wrapText = true;
sheet.showGridLines = false;
// Format dates properly.
sheet.getRange("D2:D10").format.numberFormat = "MM/DD/YYYY";
// Conditional formatting
sheet.getRange("C2:C10").conditionalFormats.addDataBar({
color: "#704023",
gradient: true,
});
sheet.getRange("E2:E10").conditionalFormats.add("cellIs", {
operator: "greaterThan",
formula: 300,
format: { font: { color: "#B91C1C" } },
});
sheet.conditionalFormattings.add({
range: "B2:B10",
rule: {
type: "expression",
formula: '=B2="Introvert"',
format: { fill: "#FCA5A5" },
},
});
// Data validation: Since Personality Type is a dropdown category, add data validation.
sheet.getRange("B2:B10").dataValidation = {
rule: {
type: "list",
values: ["Introvert", "Extrovert", "Ambivert"],
// formula1: "CategoriesSheet!$A$2:$A$4", // Alternative: reference a tunable list
},
};
// Tables: Turn it into a table (for example purposes! Make sure table names are unique)
// NOTE: If hasHeaders=true, the range must include the header row.
const table = sheet.tables.add("A1:E10", true, "PeopleTable");
table.getHeaderRowRange();
// First column is still wide since we only auto-fit the first row. Expand it manually.
sheet.getRange("A1:A10").format.columnWidth = 20;
// Create charts to the right of the table
sheet.getRange("H1:O1").merge();
sheet.getRange("H1").values = [["Charts"]];
sheet.getRange("H1").format = headerFormats;
// Adding charts
const chart = sheet.charts.add("bar", {
from: { row: 1, col: 7 },
extent: { widthPx: 620, heightPx: 320 },
});
chart.title = "Person by Scores";
chart.hasLegend = true;
chart.displayBlanksAs = "zero";
chart.legend.position = "right";
chart.barOptions.direction = "column";
chart.barOptions.grouping = "clustered";
const sheetRef = sheet.name.replaceAll("'", "''");
const dataEndRow = 4; // Keep chart refs to rows with data only.
const scoreSeries = chart.series.add("Scores by Person");
scoreSeries.categoryFormula = `'${sheetRef}'!$A$2:$A$${dataEndRow}`;
scoreSeries.formula = `'${sheetRef}'!$E$2:$E$${dataEndRow}`;
scoreSeries.valuesFormatCode = "0";
chart.setPosition("H2", "O16");
// Granular control over chart axes
chart.xAxis = {
axisType: "textAxis",
title: { text: "Person", textStyle: { fontSize: 13, bold: true } },
position: "bottom",
orientation: "minMax",
textStyle: { fontSize: 10 },
};
chart.yAxis = {
axisType: "textAxis",
title: { text: "Scores", textStyle: { fontSize: 13, bold: true } },
numberFormatCode: "0,000",
numberFormatSourceLinked: false,
};
// Chart 2 - Fastest path with defaults
const chart2 = sheet.charts.add("line", sheet.getRange("B2:C4"));
chart2.title = "Scores by Personality";
chart2.setPosition("H20", "O35");
// Sparklines: add to the right of table
const sparklinesHeader = sheet.getRange("F1");
sparklinesHeader.values = [["Sparklines"]];
sparklinesHeader.format = headerFormats;
sparklinesHeader.format.autofitColumns();
sheet.sparklines.add({
type: "column", // "line" | "column" | "stacked"
sourceData: sheet.getRange("E2:E10"),
targetRange: sheet.getRange("F2:F10"),
seriesColor: "#AAAAAA",
});
// Render
await fs.mkdir("output", { recursive: true });
const pngBlob = await workbook.render({
sheetName: "ExampleSheet",
autoCrop: "all",
scale: 1,
format: "png",
});
const pngBytes = new Uint8Array(await pngBlob.arrayBuffer());
await fs.writeFile("output/example_sheet.png", pngBytes);
console.log("Rendered first sheet to 'output/example_sheet.png'");
// Export
const out = "output/example_sheet.xlsx";
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
await xlsx.save(out);
}
await quickApiExample();
Render:
const imageBlob = await workbook.render({
sheetName: "ExampleSheet",
autoCrop: "all",
scale: 1,
format: "png",
});
Export:
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
await xlsx.save("output/spreadsheet.xlsx");