com um clique
spreadsheets
// 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.
// 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.
Use when the user asks to create, author, scaffold, package, edit, or update a plugin — a bundle with a .cowork-plugin/plugin.json manifest that groups one or more skills and, optionally, MCP servers and app integrations. Triggers on: 'create a plugin', 'new plugin', 'author a plugin', 'edit a plugin', 'update a plugin', 'add a skill to a plugin', 'bundle these skills into a plugin', 'make a plugin.json', 'add an MCP server to a plugin'.
Use when the user asks to create, author, scaffold, write, or improve a skill — a SKILL.md package that gives an agent specialized, on-demand instructions plus optional bundled resources. Triggers on: 'create a skill', 'new skill', 'write a skill', 'author a skill', 'add a skill', 'scaffold a skill', 'make a SKILL.md', 'improve this skill', 'fix my skill description'.
Transcribe local audio files on macOS 26 or newer with the `apple-native-transcribe` CLI. Use when a task needs Apple's on-device SpeechAnalyzer and SpeechTranscriber workflow for `.mp3`, `.m4a`, `.wav`, or similar local audio, and the CLI may need to be verified or installed with `brew install mweinbach/max-skills/apple-native-transcribe`. Do not use this skill on Linux, Windows, or macOS versions earlier than 26.
Create, edit, redline, and comment on `.docx` files inside the container, with a strict render-and-verify workflow. Use `render_docx.py` to generate page PNGs (and optional PDF) for visual QA, then iterate until layout is flawless before delivering the final DOCX.
Build premium editorial analytics PPTX decks with artifact-tool presentation JSX, using ruthless narrative editing, chart-first storytelling, rendered critique, and iteration until the output beats the reference deck.
| name | spreadsheets |
| description | 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 requirements and guidance for producing 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 requirements 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
Read charts.md when creating or editing substantive charts, dashboards, or chart-ready summaries.
node, system python, global npm packages, or repo-local installs.@oai/artifact-tool JS library, which exists in the default Cowork workspace dependencies node_modules, for authoring, editing, inspecting, rendering, and exporting spreadsheet .xlsx workbooks.import "@oai/artifact-tool" resolves directly from your builder — no node_modules linking, NODE_PATH tweak, or other module-resolution setup is needed..mjs builder; patch and rerun it when iterating. Do NOT use shell heredocs or keep extra builder copies.@oai/artifact-tool are unavailable, report a setup blocker; do not guess paths, install packages, use system deps, alter module resolution, copy/import bundled internals, or do a broad file system search.workbook.help("<api_or_feature>") query before building..xlsx artifact(s), one per line: [Revenue Model - MNST.xlsx](/absolute/path/to/revenue_model_mnst.xlsx).openpyxl, xlsxwriter, or pandas.ExcelWriter unless the user explicitly asks for a non-artifact-tool fallback.update_plan: build quickly, verify/render, repair meaningful issues, then finalize without long polish loops. Incrementally rendering your work and assessing overall aesthetics, formatting and correctness along the way is very important (rigorously inspect the output and be confident in quality), but do not get stuck in a long render-verify loop. As part of your plan, think about the best practices and conventions to follow for the specific type of spreadsheet you're creating and the best way to structure the workbook for readability and usability.Read these domain templates only when the request clearly relates to the domain:
templates/financial_models.mdtemplates/healthcare.mdtemplates/marketing_advertising.mdtemplates/scientific_research.mdRead all templates that clearly apply. Do not load domain templates for unrelated routine, lightweight, or basic formatting/editing tasks unless the user explicitly asks.
Only add Checks sheets for models where correctness depends on linked calculations, source reconciliation, or financial statement/model integrity.
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.pypdf when 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.pandas, numpy, pypdf, python-docx, and reportlab.docx, pdf-lib, and pdfjs-dist.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.workbook.trace("Sheet!A1") on the key output/check cells. It takes only a cell reference and returns the full tree, so print a capped summary. Do not dump raw traces.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.workbook.help("fx.PMT", { include: "index,examples,notes", maxChars: 3000 }).fx.* with a category regex. Useful categories: financial, math-trig, statistical, lookup-reference, logical, text, date-time, information, engineering, database.search regex of likely functions.maxChars bounded; if results are noisy, narrow search rather than issuing many similar queries.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,
);
console.log(workbook.help("fx.PMT", { include: "index,examples,notes" }).ndjson);
console.log(workbook.help("fx.*", { search: "financial", include: "index,examples", maxChars: 4000 }).ndjson);
console.log(workbook.help("fx.*", { search: "math-trig", include: "index,examples", maxChars: 4000 }).ndjson);
console.log(workbook.help("lookup with fallback", { search: "XLOOKUP|INDEX|MATCH|IFERROR", include: "index,examples,notes", maxChars: 4000 }).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.workbook.trace("Sheet!A1") to audit the dependency tree from final output/check cell back to source cells. Trace output can be large, so summarize by depth/node count before logging.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 threadrange.merge() merges the target range into one cell; range.merge(true) merges across each row in the target range.range.unmerge() reverses a merge.
For example:const range = sheet.getRange("I23:N24");
range.merge();
range.values = [["Source note spanning the recommendation panel"]];
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.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 trace = workbook.trace("Checks!F2") // summarize before loggingconst 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(ruleType, ConditionalFormatConfig);.range.conditionalFormats.add(ruleType, {operator, formula, format});. Choose ruleType, operator, color, and style strings from the inline types below.type ConditionalFormatRuleType =
| "cellIs" | "CellValue" | "Custom" | "expression"
| "colorScale" | "dataBar" | "iconSet"
| "containsText" | "notContainsText" | "beginsWith" | "endsWith"
| "containsBlanks" | "notContainsBlanks" | "containsErrors" | "notContainsErrors"
| "duplicateValues" | "uniqueValues" | "timePeriod" | "top10" | "aboveAverage";
type CellIsOperator =
| "greaterThan"
| "greaterThanOrEqual"
| "lessThan"
| "lessThanOrEqual"
| "equal"
| "notEqual"
| "between"
| "notBetween";
type ConditionalFormatConfig =
| { operator: CellIsOperator; formula: string | number | Array<string | number>; format?: DifferentialFormatConfig }
| { formula: string | number; format?: DifferentialFormatConfig }
| { colors?: ColorConfig[]; thresholds?: CfvoInput[] }
| { color?: ColorConfig; thresholds?: CfvoInput[]; gradient?: boolean }
| { iconSet: string; showValue?: boolean; reverse?: boolean; thresholds?: CfvoInput[] }
| { text: string; format?: DifferentialFormatConfig }
| { timePeriod: "yesterday" | "today" | "tomorrow" | "last7Days" | "lastWeek" | "thisWeek" | "nextWeek" | "lastMonth" | "thisMonth" | "nextMonth"; format?: DifferentialFormatConfig }
| { rank?: number; percent?: boolean; bottom?: boolean; format?: DifferentialFormatConfig }
| { aboveAverage?: boolean; equalAverage?: boolean; stdDev?: number; format?: DifferentialFormatConfig };
type DifferentialFormatConfig = {
fill?: FillConfig;
font?: { bold?: boolean; italic?: boolean; color?: ColorConfig };
border?: RangeBordersConfig;
numberFormat?: string;
};
type CfvoInput =
| "min"
| "max"
| number
| `${number}%`
| { type: "min" | "max" | "num" | "percent" | "percentile"; value?: string | number };
ConditionalFormatRuleType): "cellIs" | "CellValue" | "Custom" | "expression"
| "colorScale" | "dataBar" | "iconSet"
| "containsText" | "notContainsText" | "beginsWith" | "endsWith"
| "containsBlanks" | "notContainsBlanks" | "containsErrors" | "notContainsErrors"
| "duplicateValues" | "uniqueValues" | "timePeriod" | "top10" | "aboveAverage";iconSet names: 3Arrows, 3Triangles, 4Arrows, 5Arrows, 3ArrowsGray, 4ArrowsGray, 5ArrowsGray, 3TrafficLights1, 3Signs, 4RedToBlack, 3TrafficLights2, 4TrafficLights, 3Symbols, 3Flags, 3Symbols2, 3Stars, 5Quarters, 5Boxes, 4Rating, 5Rating.range.conditionalFormats.addCustom(expression, {fill, font, border});range.conditionalFormats.deleteAll() / range.conditionalFormats.clear()const grid = sheet.getRange("B2:J10");
grid.conditionalFormats.add("colorScale", {
criteria: [
{ type: "lowestValue", color: "#2563EB" },
{ type: "percentile", value: 50, color: "#FDE047" },
{ type: "highestValue", color: "#DC2626" },
],
});
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.images.add({dataUrl: "data:image/png;base64,...", anchor: {from: { row: 1, col: 2 }, extent: { widthPx: 160, heightPx: 120 }}})
### Threaded Comments
Follow this exact API when adding a note or comment:
- Required: First, always first call set_self to create an author `workbook.comments.setSelf({"displayName": "ChatGPT"})`
- Create a new thread with a single comment: `const thread = workbook.comments.addThread({"cell": sheet.getRange("E2")}, "Source: <website>")`
- To reply to a threaded comment: `thread.addReply("This is a reply to the comment")`
- To resolve/re-open a thread: `thread.resolve()`, `thread.reopen()`
### Charts
- When adding or moving charts, do not cover existing data. Put charts in a reserved rectangle with blank gutter columns/rows around the chart area.
- Fast chart path, no help lookup needed for common line/bar/scatter charts: write a compact helper range with text categories and one column per series, then chart that range.
- If chart data comes from editable/source data, make the helper range formula-backed instead of copying literal values.
```js
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 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".const group = sheet.sparklineGroups.add({
type,
targetRange,
sourceData,
dateAxisRange,
seriesColor,
negativeColor,
markers,
axis,
lineWeight,
displayEmptyCellsAs,
displayHidden,
});
type SparklineConfig = {
type: "line" | "column" | "stacked";
targetRange: Range | string;
sourceData: Range | string;
dateAxisRange?: Range | string;
lineWeight?: number;
displayHidden?: boolean;
seriesColor?: ColorConfig;
negativeColor?: ColorConfig;
axisColor?: ColorConfig;
markersColor?: ColorConfig;
firstMarkerColor?: ColorConfig;
lastMarkerColor?: ColorConfig;
highMarkerColor?: ColorConfig;
lowMarkerColor?: ColorConfig;
markers?: SparklineMarkersOptions;
axis?: SparklineAxisOptions;
};
type SparklineMarkersOptions = {
show?: boolean;
high?: boolean;
low?: boolean;
first?: boolean;
last?: boolean;
negative?: boolean;
};
type SparklineAxisOptions = {
showAxis?: boolean;
manualMin?: number;
manualMax?: number;
rightToLeft?: boolean;
};
const group = targetRange.sparklines.add(type, sourceRange, sparklineConfig);group.seriesColor = colorConfig;
group.markers = markerConfig;
group.axis = axisConfig;
group.delete();
sheet.sparklineGroups.deleteAll();
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("fx.RATE", { include: "index,examples,notes" }).ndjsonworkbook.help("cash flow return rate", { search: "IRR|XIRR|NPV|XNPV", include: "index,examples,notes", maxChars: 4000 }).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";
const outputDir = "output";
await fs.mkdir(outputDir, { recursive: true });
const workbook = Workbook.create();
const sheet = workbook.worksheets.add("Summary");
sheet.getRange("A1:C4").values = [
["Month", "Revenue", "EBITDA"],
["Jan", 100, 10],
["Feb", 120, 18],
["Mar", 130, 22],
];
sheet.getRange("D1").values = [["Margin"]];
sheet.getRange("D2").formulas = [["=C2/B2"]];
sheet.getRange("D2:D4").fillDown();
sheet.getRange("A1:D1").format = {
fill: "#0F766E",
font: { bold: true, color: "#FFFFFF" },
};
sheet.getRange("B2:C4").format.numberFormat = "$#,##0";
sheet.getRange("D2:D4").format.numberFormat = "0.0%";
// Helper range links to source cells so edits update the chart.
sheet.getRange("F1:G1").values = [["Month", "Revenue"]];
sheet.getRange("F2:G2").formulas = [["=A2", "=B2"]];
sheet.getRange("F2:G4").fillDown();
const chart = sheet.charts.add("line", sheet.getRange("F1:G4"));
chart.title = "Revenue Trend";
chart.hasLegend = false;
chart.xAxis = { axisType: "textAxis" };
chart.yAxis = { numberFormatCode: "$#,##0" };
chart.setPosition("I1", "P15");
const preview = await workbook.render({
sheetName: "Summary",
autoCrop: "all",
scale: 1,
format: "png",
});
await fs.writeFile(`${outputDir}/summary.png`, new Uint8Array(await preview.arrayBuffer()));
const xlsx = await SpreadsheetFile.exportXlsx(workbook);
await xlsx.save(`${outputDir}/summary.xlsx`);