Find out how to integrate ChatGPT in Sheets with custom functions and a handy template to help you run tasks in bulk.
ChatGPT is great for quick questions, ideation and connecting docs or other media like images. However, your work style may have you stuck in Sheets and you may find data entry-type tasks more efficient to perform end-to-end in one place. That's why directly integrating OpenAI inside Sheets can become a more efficient and flexible way to perform sizeable tasks. There is no need to waste time uploading and downloading docs and sort multiple file versions.
In this guide, I'm providing you with a template and a walkthrough on scripting.
Prerequisites
- OpenAI API key
Create a new API key (and Organisation Project) or ask your tech admin to generate a unique one for you.
Save the key somewhere safe and private as soon as it's generated.
- G-account to use Google Sheets
Google Sheets is free to use and is possible to get started with another email address provider e.g. Outlook.
Setting up
To get you running quickly, make the template yours by creating a copy, adding your API key and authorizing the Apps Script. You could then run the functions with the sample data there (in column F) as an initial test.
1. Make a copy of the Sheet template here.
2. Next, you'll need to add your OpenAI API key. Go to Extensions > Apps Script > βοΈ Project Settings > Script Properties > Add script property
For the 'Property' enter:
OPENAI_API_KEY
For 'Value' paste in your secret API key and then 'Save script properties'.
In the general settings above, you should also check that the project Time zone is set to your local regional time zone.
3. Then you'll need to allow the Apps Script in order to connect OpenAI.
Next, go to Extensions > Apps Script > Editor > Debug
From the popup window, click 'Review permissions' and sign into Google if asked to.
Then click 'Advanced' > Go to 'gpt4o'
And finally, click 'Allow'.
Configuring the ChatGPT prompts
The script file you'll need to adjust is the 'config.gs' in the Apps Script Files Editor. This is where the main configurations are for writing instructions to ChatGPT. So, all you need to do is locate the "SYSTEM_PROMPT_PREPEND" AND "SYSTEM_PROMPT_APPEND" at the top of the file and swap out what's there between the straight apostrophes with your own prompts.
var SYSTEM_PROMPT_PREPEND = 'You are a search engine marketing expert. Please review the following keyword search intent and propose an article based on it:';
var SYSTEM_PROMPT_APPEND = 'Write your new article proposal in the form of a synopsis that could directly brief a content writer. Your response should be in plain text.';
This is the structure of what is sent to GPT:
i) Prepended prompt
e.g. "You are a search engine marketing expert. Please review the following keyword search intent and propose an article based on it:"
ii) Sheet data cell(s) - from the 'Input' column F
e.g. "how to train a puppy"
iii) Appended prompt
e.g. "Write your new article proposal in the form of a synopsis that could directly brief a content writer. Your response should be in plain text."
and then GPT's response will appear in the 'GPT Output' column G.
Generally, it's good prompt engineering practice to prepend a 'role' for the GPT, such as "You are a French translator" and give it a goal such as "You will identify the following excerpt's original language and translate it into French".
Following this "Prepended" prompt, will be data from the main Sheet "Input" column for context.
Then, you can append more specific expectations to your context.
Tip: Microsoft's perfect prompt formula is a good one to keep in mind when prompt engineering: 'goal + context + sources + expectations'. Wherein, a clear goal should be the heart of the prompt. Context is necessary for guiding the response. Sources and Expectations make the response more precise.
Running the custom functions
After saving your customisations to the 'System prompts for callGPT functions', go back to the main sheet and refresh the page.
Add context anywhere in column F: Input.
Now, you can either type the custom formula function "=GPT_OUTPUT" in any cell and use any cell location in the formula builder. Or use the toolbar menu "OpenAI Integration β¨" > "π€ Process Outputs" to send the prepended/context input/appended prompt and fetch GPT's response.
By using the custom formula, you can use multiple rows of cells to be used as context in the prompt.
By using the custom function "π€ Process Outputs" from the menu dropdown, you can run prompts per each row in automated batches. The default number of rows in a batch is 10, so you may experience an initial delay before seeing any responses.
The 2nd custom function you probably noticed, is the 'GPT_SUMMARY'/'π§βπ« Process Summaries'. These are handy for summarising long-form responses in the 'GPT output' column G.
Secret Tip: in the spirit of Pride and to brighten up your Sheets, type "PRIDE" across the first five columns in your Sheet.
Other useful functions
Firstly, I want to point out that there is a 'ProgressTracker' tab. This is for automatically recording the Sheet row number processed in case of time outs or if you added new rows of input that you want to process at a later time. You will need to remember to reset it to "2" (discounting the header row #1 in the "ChatGPT function examples" tab) whenever you clear the columns or edit the prompts and want to restart from row 2.
Tip: use 'Reset Progress Tracker π' in the toolbar menu to do it for you.
I will also point out that the purpose of final tab called 'Logs' is to log any errors you may encounter with OpenAI, such as issues with token limits or restrictions to ChatGPT's capabilities.
What do the custom toolbar menu functions do?
In the 'OpenAI Integration β¨' custom toolbar menu dropdown:
- 'π€ Process Outputs' takes whatever is in the Input column and sends it along inside your prompt to OpenAI row-by-row and returns ChatGPT's response in the corresponding row (in column G). It will automatically process rows in batches of 10 and stop when an empty Input cell is detected.
- 'π§βπ« Process Summaries' similarly takes whatever is in the GPT output column and sends it along inside your summary prompt to OpenAI row-by-row and returns ChatGPT's response in the corresponding row (in column H). It will automatically process rows in batches of 10 and stop when an empty GPT output cell is detected.
- 'Clear Input π«₯' deletes everything in column F (in the 'ChatGPT function examples' tab, excluding the heading)
- 'Clear Output π«₯' deletes everything in column G
- 'Clear Summary π«₯' deletes everything in column H
- 'Make formulas static π' overwrites any cell with the custom formulas with the value (ChatGPT's response)
- 'Reset Progress Tracker π' overwrites the row number recorded in the ProgressTracker tab (cell A2). The numbering reflects the Sheet row numbers in the 'ChatGPT function examples' tab.
Tip: remember to Reset the Progress Tracker before running the Process Summaries function or whenever you try a new prompt. You can also manually type in a row number you want to GPT to rerun or continue from.
Adjusting the OpenAI API configuration
In the config.gs file, locate the API and token configuration lines.
// API configuration
var API_URL = 'https://api.openai.com/v1/chat/completions';
var MODEL_VERSION = 'gpt-4o-mini';
var MODEL_TEMPERATURE = 0.7; // Default temperature
var BATCH_SIZE = 10; // Number of rows to process per batch
// Maximum tokens configuration
var MAX_TOTAL_TOKENS = 4096;
var DEFAULT_MAX_COMPLETION_TOKENS_OUTPUT = 8000; //100 tokens ~= 75 words
var DEFAULT_MAX_COMPLETION_TOKENS_SUMMARY = 150;
Generally, you may only need to change the OpenAI API model and Temperature.
At the time of writing, gpt4o mini is the latest model that this template is using. It is very affordable and close in speed to the old GPT3.5 turbo model. However, you can swap this out with your preferred model.
The model temperature should be a decimal between 0 and 1. The closer to 0, then the more rigid the response will be. The closer to 1, then the more creative and spontaneous the response will be.
Tip: be mindful of straight (") vs curly (β) quotation or apostraphe (' vs β) marks when editing code.
If you're interested in further understanding and customising the integration continue reading the next section.
Making your custom OpenAI API integration in Sheets
You'll see in the template Apps Script project that there are 7 different files:
1. config.gs: contains global configuration settings for the batch function and custom cell formula calls to OpenAI API with token optimisation. The token calculator is custom-made and won't charge you OpenAI tokens, but using the GPT functions/scripts will be charged by OpenAI. It helps prevent timeouts and truncated or issue responses from ChatGPT.
// System prompts for callGPT functions
var SYSTEM_PROMPT_PREPEND = 'You are a search engine marketing expert. Please review the following keyword search intent and propose an article based on it:'; //Prepend a role and instructions for tasking the GPT
var SYSTEM_PROMPT_APPEND = 'Write your new article proposal in the form of a synopsis that could directly brief a content writer. Your response should be in plain text.'; // Additional prompt to append if needed
// System prompts for GPTsummarise functions
var SUMMARY_PROMPT_PREPEND = 'You are a British English editor. Please review the following text:'; //Prepend a role and instructions for tasking the GPT
var SUMMARY_PROMPT_APPEND = 'Summarize in plain text as a bullet point list of the most important points and use relevant emojis to highlight key points while staying true to the original content.'; // Additional prompt to append if needed
// Retrieve the API key securely from Project Settings > Script Properties
function getOpenAIKey() {
return PropertiesService.getScriptProperties().getProperty('OPENAI_API_KEY');
}
// API configuration
var API_URL = 'https://api.openai.com/v1/chat/completions';
var MODEL_VERSION = 'gpt-4o-mini';
var MODEL_TEMPERATURE = 0.7; // Default temperature
var BATCH_SIZE = 10; // Number of rows to process per batch
// Maximum tokens configuration
var MAX_TOTAL_TOKENS = 4096; // check the max limit by OpenAI model version and your subscription tier
var DEFAULT_MAX_COMPLETION_TOKENS_OUTPUT = 8000; //100 tokens ~= 75 words
var DEFAULT_MAX_COMPLETION_TOKENS_SUMMARY = 150;
function getMaxCompletionTokens(prompt, defaultMaxCompletionTokens) {
const promptTokens = estimateTokens(prompt);
return Math.min(defaultMaxCompletionTokens, MAX_TOTAL_TOKENS - promptTokens);
}
// Utility function to estimate token count can be adjusted for the language or type of input
function estimateTokens(text) {
// Simple estimation here: 1 token per 4 characters is the average for English text then rounded up
return Math.ceil(text.length / 4);
}
2. callGPT.gs: Handles calling the OpenAI API with token calculation to perform the main task and generate a response in the 'GPT output' Sheet column.
function callOpenAIChat(input) {
const apiKey = getOpenAIKey();
const url = API_URL;
const prompt = SYSTEM_PROMPT_PREPEND + input + SYSTEM_PROMPT_APPEND; // System prompt prepend and append from the config.gs file and input is taken from the Input column F are added together to estimate initial token usage
const maxCompletionTokens = getMaxCompletionTokens(prompt, DEFAULT_MAX_COMPLETION_TOKENS_OUTPUT); // the number of completion tokens calculated from subtracting your max limit from the initial prompt usage estimate
const payload = {
model: MODEL_VERSION,
messages: [
{ role: 'system', content: SYSTEM_PROMPT_PREPEND + SYSTEM_PROMPT_APPEND },
{ role: 'user', content: input }
],
max_tokens: maxCompletionTokens,
temperature: MODEL_TEMPERATURE
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': `Bearer ${apiKey}`
},
payload: JSON.stringify(payload)
};
try {
const response = UrlFetchApp.fetch(url, options);
const json = response.getContentText();
const data = JSON.parse(json);
const output = data.choices[0].message.content.trim();
if (isValidResponse(output)) {
return output;
} else {
logUniqueError('Invalid Response', `Input: ${input}, Output: ${output}`);
return 'Error';
}
} catch (error) {
logUniqueError('API Error', `Input: ${input}, Error: ${error.message}`);
return 'Error';
}
}
3. GPTsummarise.gs: Handles calling the OpenAI API for generating GPT summaries in the 'GPT summarised' Sheet column. You could rework this to perform a different follow up task or make a copy of this to create a third follow up task for GPT to perform.
function callOpenAISummarize(input) {
const apiKey = getOpenAIKey();
const url = API_URL;
const prompt = SUMMARY_PROMPT_PREPEND + input + SUMMARY_PROMPT_APPEND;
const maxCompletionTokens = getMaxCompletionTokens(prompt, DEFAULT_MAX_COMPLETION_TOKENS_SUMMARY);
const payload = {
model: MODEL_VERSION,
messages: [
{ role: 'system', content: SUMMARY_PROMPT_PREPEND + SUMMARY_PROMPT_APPEND },
{ role: 'user', content: input }
],
max_tokens: maxCompletionTokens,
temperature: MODEL_TEMPERATURE
};
const options = {
method: 'post',
contentType: 'application/json',
headers: {
'Authorization': `Bearer ${apiKey}`
},
payload: JSON.stringify(payload)
};
try {
const response = UrlFetchApp.fetch(url, options);
const json = response.getContentText();
const data = JSON.parse(json);
const output = data.choices[0].message.content.trim();
if (isValidResponse(output)) {
return output;
} else {
logUniqueError('Invalid Response', `Input: ${input}, Output: ${output}`);
return 'Error';
}
} catch (error) {
logUniqueError('API Error', `Input: ${input}, Error: ${error.message}`);
return 'Error';
}
}
4. sheetFunctions.gs: Contains functions to process outputs, summaries, clear columns, make custom functions static, and reset the Progress Tracker.
function processOutputs() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
let lastProcessedRow = getLastProcessedRow();
const batchSize = BATCH_SIZE;
while (lastProcessedRow - 1 < data.length) {
const end = Math.min(lastProcessedRow + batchSize - 1, data.length);
const batch = data.slice(lastProcessedRow - 1, end);
batch.forEach((row, index) => {
if (row[5] && !row[6]) { // Column F (index 5) is input, Column G (index 6) is output
const input = row[5];
const output = callOpenAIChat(input);
if (output !== 'Error') {
sheet.getRange(lastProcessedRow + index, 7).setValue(output); // Write the output to column G (index 6)
}
}
});
lastProcessedRow = end + 1;
setLastProcessedRow(lastProcessedRow);
SpreadsheetApp.flush();
}
}
function processSummaries() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const data = sheet.getDataRange().getValues();
let lastProcessedRow = getLastProcessedRow();
const batchSize = BATCH_SIZE;
while (lastProcessedRow - 1 < data.length) {
const end = Math.min(lastProcessedRow + batchSize - 1, data.length);
const batch = data.slice(lastProcessedRow - 1, end);
batch.forEach((row, index) => {
if (row[6] && !row[7]) { // Column G (index 6) is output, Column H (index 7) is summary
const output = row[6];
const summary = callOpenAISummarize(output);
if (summary !== 'Error') {
sheet.getRange(lastProcessedRow + index, 8).setValue(summary); // Write the summary to column H (index 7)
}
}
});
lastProcessedRow = end + 1;
setLastProcessedRow(lastProcessedRow);
SpreadsheetApp.flush();
}
}
function clearColumn(column) {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const range = sheet.getRange(2, column, sheet.getLastRow() - 1, 1); // Start from row 2 to avoid headers
range.clearContent();
}
function clearInput() {
clearColumn(6); // Column F (index 6)
}
function clearOutput() {
clearColumn(7); // Column G (index 7)
}
function clearSummary() {
clearColumn(8); // Column H (index 8)
}
function makeCustomFunctionsStatic() {
const sheet = SpreadsheetApp.getActiveSpreadsheet().getActiveSheet();
const range = sheet.getDataRange();
const formulas = range.getFormulas();
const values = range.getValues();
for (let row = 0; row < formulas.length; row++) {
for (let col = 0; col < formulas[row].length; col++) {
if (formulas[row][col].startsWith('=GPT_OUTPUT') || formulas[row][col].startsWith('=GPT_SUMMARY')) {
sheet.getRange(row + 1, col + 1).setValue(values[row][col]);
}
}
}
}
function resetProgressTracker() {
const progressTrackerSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ProgressTracker');
progressTrackerSheet.getRange('A2').setValue(2);
}
5.Β helpers.gs: Contains JS helper functions for progress tracking, error handling, retry logic.
function getLastProcessedRow() {
const progressTrackerSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ProgressTracker');
return parseInt(progressTrackerSheet.getRange('A2').getValue());
}
function setLastProcessedRow(row) {
const progressTrackerSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('ProgressTracker');
progressTrackerSheet.getRange('A2').setValue(row);
}
function logUniqueError(type, message) {
const logSheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Logs');
const existingErrors = logSheet.getDataRange().getValues();
const errorIndex = existingErrors.findIndex(row => row[1] === message);
if (errorIndex > -1) {
// If error message already exists, append the affected row number
logSheet.getRange(errorIndex + 1, 3).setValue(logSheet.getRange(errorIndex + 1, 3).getValue() + `, ${getLastProcessedRow()}`);
} else {
// Log new error with the initial row number
logSheet.appendRow([new Date(), type, message, getLastProcessedRow()]);
}
}
function isValidResponse(response) {
// Basic validation example: ensure non-empty response
return response && response.length > 20; // Example condition
}
6. menuUI.gs: Creates the custom toolbar menu in Google Sheets.
function onOpen() {
const ui = SpreadsheetApp.getUi();
ui.createMenu('OpenAI Integration β¨')
.addItem('π€ Process Outputs', 'processOutputs')
.addItem('π§βπ« Process Summaries', 'processSummaries')
.addItem('Clear Input π«₯', 'clearInput')
.addItem('Clear Output π«₯', 'clearOutput')
.addItem('Clear Summary π«₯', 'clearSummary')
.addItem('Make formulas static π', 'makeCustomFunctionsStatic')
.addItem('Reset Progress Tracker π', 'resetProgressTracker')
.addToUi();
}
7. customFunctions.gs: Contains custom functions for in-cell use (GPT_OUTPUT and GPT_SUMMARY formulas).
/**
* Generates a GPT output for the given input.
* @param {string} input The input text to process.
* @return {string} The GPT output.
* @customfunction
*/
function GPT_OUTPUT(input) {
return callOpenAIChat(input);
}
/**
* Generates a summary for the given GPT output.
* @param {string} output The GPT output to summarize.
* @return {string} The summarized output.
* @customfunction
*/
function GPT_SUMMARY(output) {
return callOpenAISummarize(output);
}
More considerations
Hopefully you've been inspired to continue testing and building on this template. There are many great possibilities in other OpenAI API endpoints and types of input such as images.
Tip: Google Sheets IMAGE function can render web images in your Sheet cell!
You could even integrate another AI API or LLM to compare different AI responses in one place.
Scripting in Google Sheets is great for scaling medium to large-size automation with GenAI. However, there comes a file size limit when too many rows slow down your app and it's best to move into a different framework using a larger database such as Google Cloud Functions and BigQuery or open-source providers Vercel AI SDK, NextJS and PostgreSQL.
Besides the Apps Scripts there are other ways to connect apps with Google owned apps (Looker Studio and BigQuery) or Sheet Add-ons which unlock incredible possibilities.