Google Sheets integration
Claude and GPT in Google Sheets with an =ASK_AI() formula
One 108-line Apps Script adds =ASK_AI() to Google Sheets: paste it into Extensions > Apps Script, save your p2a_ key as a Script Property and type =ASK_AI(A2) to get a reply from Claude, GPT, Gemini or Kimi through Plan2API. Each formula call must finish within 30 seconds.
- Formula
- =ASK_
AI(prompt, model) - Limit per call
- 30 seconds
- URL Fetch calls a day
- 20,000 (personal account)
- Key stored in
- Script Properties
Updated
Google Sheets settings at a glance
This table lists every setting and value the ASK_AI script uses to run an AI formula in Google Sheets. You can change the defaults right in the script editor if you upgrade your plan or want a different fallback model.
Script properties are shared by everyone who can edit the script, and anyone with edit access to the spreadsheet can open the script editor, so give each shared spreadsheet its own p2a_ key.
| Setting | Value |
|---|---|
| Endpoint | https:// |
| Auth header | Authorization: Bearer p2a_ |
| Script Property | PLAN2API_ set to your p2a_ key |
| Default model | claude/ |
| Formula | =ASK_ or =ASK_ |
| Time limit | 30 seconds per formula call |
Add =ASK_AI() to a spreadsheet
- Sign up at Plan2API with a Google account, connect a Claude, ChatGPT, Google AI or Kimi subscription, and copy your
p2a_key. - Open your spreadsheet and select
Extensions > Apps Scriptfrom the top menu. - Delete any code that's already in the editor, paste in the script from the next section, and click
Saveat the top. - On the left sidebar, click
Project Settings, scroll down toScript Properties, and clickAdd script property. - Enter
PLAN2API_KEYas the Property and paste yourp2a_key as the Value, then clickSave script properties. - Optionally, run
testPlan2APIfrom the script editor to check the key. Unlike the formula, a function run from the editor can ask you to authorize the script first; it then logs the reply. - Back in the sheet, type
=ASK_AI("Write a tagline for a bakery")or=ASK_AI(A2)into any cell; it'll showLoading...for a moment and then drop in the reply.
Custom functions in Google Sheets never ask for authorization, and because URL Fetch is one of the services they're allowed to call directly, the formula works right away without an approval prompt.
The Apps Script code
The script below pulls PLAN2API_KEY from your Script Properties and sends each prompt to POST /v1/chat/completions at https://api.plan2api.com/v1 using an Authorization: Bearer header. It sends up to 4 requests at once through UrlFetchApp.fetchAll, which matches the Free plan's concurrency limit, though you can bump PARALLEL_REQUESTS up to 16 on Pro or 32 on Scale. It also watches a 27-second deadline (DEADLINE_SECONDS) and stops starting new batches when less than 5 seconds remain so the cell never hits Google's 30-second #ERROR! cutoff, marking any leftover rows as Skipped and writing API errors into the cell as plain text.
const PLAN2API_URL = 'https://api.plan2api.com/v1/chat/completions';
const DEFAULT_MODEL = 'claude/claude-sonnet-5';
const DEADLINE_SECONDS = 27;
// Free allows 4 requests at once, Pro 16 and Scale 32
const PARALLEL_REQUESTS = 4;
/**
* Answers a prompt with Claude, GPT, Gemini or Kimi through Plan2API.
*
* @param {string|Array<Array<string>>} prompt Text, a cell or a one-column range.
* @param {string} model Optional Plan2API model id, for example "kimi/k3".
* @return The reply, or one reply per row of the range.
* @customfunction
*/
function ASK_AI(prompt, model) {
const apiKey = PropertiesService.getScriptProperties().getProperty('PLAN2API_KEY');
if (!apiKey) {
return 'Add PLAN2API_KEY in Project Settings > Script Properties';
}
const modelId = model ? String(model).trim() : DEFAULT_MODEL;
if (!Array.isArray(prompt)) {
return askAll([toText(prompt)], modelId, apiKey)[0];
}
const replies = askAll(prompt.map(row => toText(row[0])), modelId, apiKey);
return replies.map(reply => [reply]);
}
function askAll(prompts, modelId, apiKey) {
const startedAt = Date.now();
const replies = prompts.map(() => '');
const pending = [];
prompts.forEach((text, index) => {
if (text) {
pending.push({ text: text, index: index });
}
});
for (let start = 0; start < pending.length; start += PARALLEL_REQUESTS) {
const batch = pending.slice(start, start + PARALLEL_REQUESTS);
const secondsLeft = DEADLINE_SECONDS - Math.ceil((Date.now() - startedAt) / 1000);
// Google stops a custom function after 30 seconds, so we skip rows that can't finish in time
if (secondsLeft < 5) {
pending.slice(start).forEach(item => {
replies[item.index] = 'Skipped: 30-second limit reached, use a smaller range';
});
break;
}
try {
const requests = batch.map(item => buildRequest(item.text, modelId, apiKey, secondsLeft));
UrlFetchApp.fetchAll(requests).forEach((response, i) => {
replies[batch[i].index] = readReply(response);
});
} catch (error) {
batch.forEach(item => {
replies[item.index] = 'Error: ' + error.message;
});
}
}
return replies;
}
function buildRequest(text, modelId, apiKey, timeoutSeconds) {
return {
url: PLAN2API_URL,
method: 'post',
contentType: 'application/json',
headers: { Authorization: 'Bearer ' + apiKey },
payload: JSON.stringify({
model: modelId,
messages: [{ role: 'user', content: text }]
}),
muteHttpExceptions: true,
timeoutSeconds: timeoutSeconds
};
}
function readReply(response) {
const status = response.getResponseCode();
let body;
try {
body = JSON.parse(response.getContentText());
} catch (error) {
return 'Error ' + status + ': the response was not JSON';
}
if (status !== 200) {
const error = body.error || {};
return ['Error', status, error.code].filter(Boolean).join(' ') + ': ' + (error.message || 'request failed');
}
const choice = body.choices && body.choices[0];
const content = choice && choice.message && choice.message.content;
return content ? content.trim() : 'Error: the model returned an empty reply';
}
function toText(value) {
return value === null || value === undefined ? '' : String(value).trim();
}
function testPlan2API() {
Logger.log(ASK_AI('Reply with one word: ready'));
}Pick a model per formula
Pass a model id as the second argument to override the default Claude Sonnet 5 (claude/claude-sonnet-5), like =ASK_AI(A2, "gemini/gemini-3.8-flash-high"). You can also point that second argument at a cell reference such as =ASK_AI(A2, $B$1) so you can switch the model for every row at once just by editing B1.
You can pass a whole one-column range like =ASK_AI(A2:A20) to answer all those rows in a single formula, and the results will spill down into the cells below as long as those cells are empty. Codex model ids such as openai/gpt-6.1-sol, openai/gpt-6-astra and openai/gpt-6-luna run on your own ChatGPT plan, so pick one your plan includes; GET /v1/models lists every id Plan2API serves.
| Model id | Subscription | Context window |
|---|---|---|
claude/ | Claude Pro or Max | 1M tokens |
claude/ | Claude Pro or Max | 200K tokens |
openai/ | ChatGPT plan (Codex) | Not published |
gemini/ | Google AI Pro or Ultra | 1,048,576 tokens |
kimi/ | Kimi Code membership | 1,048,576 tokens |
What works and what doesn't
- Text prompts work with every chat model in both single cells and one-column ranges.
- A formula recalculates only when you edit it or change a cell it references, and every recalculation fires off new API requests.
- Volatile functions like
NOW()andRAND()aren't allowed inside custom function arguments, and any cell that depends on them will sit onLoading...forever. - Any execution that runs past 30 seconds fails with
#ERROR!and the cell noteExceeded maximum execution time (line 0), so ask for short answers and keep ranges small. - Google caps URL Fetch calls at 20,000 a day for personal Google accounts and 100,000 a day for Google Workspace accounts.
- On the Free plan, each prompt in a cell or range counts as one of your 50 daily requests.
- This script sends text only, so images and JSON mode aren't wired up, and Claude models will reject a non-text
response_formatwith400unsupported_parameterif you modify the payload to add one.
Fix ASK_AI errors
Because the script uses muteHttpExceptions and writes errors straight into the cell as text (like Error 400 model_not_found: followed by the details), the cell output tells you what needs fixing.
Script invoked too many times per second for this Google user account happens when you drag or fill the formula down hundreds of individual rows at once; use a single range formula like =ASK_AI(A2:A20) so Sheets runs one execution instead of one per cell.
| Cell shows | Cause | Fix |
|---|---|---|
| Add PLAN2API_ | Script Property missing or misnamed | Add PLAN2API_ exactly |
Error 401 invalid_ | Wrong or partial key | Paste the full p2a_ key again |
Error 403 no_ | No subscription for that provider | Connect it or change the model |
Error 400 model_ | Typo in the model id | Copy an id from GET / |
Error 429 daily_ | Free plan's 50 requests used | Wait for 00:00 UTC or upgrade |
| Skipped: 30-second limit reached | Range too big for one call | Split the range |
| #ERROR! Exceeded maximum execution time | One reply took over 30 seconds | Ask for a shorter answer |
| Loading... that never ends | NOW() or RAND() in the arguments | Reference plain cells |
Use your Claude or ChatGPT plan in Google Sheets
Plan2API turns a Claude Pro or Max, ChatGPT (through Codex), Google AI Pro or Ultra (through Antigravity), or Kimi (through Kimi Code) subscription you already pay for into one p2a_ API key. Your =ASK_AI() formulas run straight against that subscription, and Plan2API never bills per token.
Your subscription's own usage limits still apply, and Plan2API doesn't add a token quota of its own on top. If you connect more than one account from the same provider, a request that hits a limit automatically rolls over to your next connected account before any output is sent. You can check the full plan details on the pricing page:
- Free is $0 with no card needed, and gives you 1 connected subscription, 50 chat requests a day (resetting at 00:00 UTC), and 4 requests at once.
- Pro is $12 a month or $99 a year for 3 connected subscriptions, no daily request cap, and 16 requests at once.
- Scale is $29 a month or $249 a year for 25 connected subscriptions, no daily request cap, and 32 requests at once.
Use the AI subscriptions you already pay for as one OpenAI-compatible API key.
- Free: 1 subscription, 50 requests a day, no card
- Pro: $12 a month
FAQ
How do I use Claude in Google Sheets?
Paste the ASK_AI Apps Script into your sheet's script editor, save your p2a_ key as the PLAN2API_KEY script property, and type =ASK_AI(A2, "claude/claude-sonnet-5") into a cell. Since claude/claude-sonnet-5 is already the default model in the script, =ASK_AI(A2) uses Claude too, running on your connected Claude Pro or Max subscription through Plan2API.
Can I use GPT in Google Sheets with my ChatGPT subscription?
Yes, connect your ChatGPT account to Plan2API using a one-time device code and pass a Codex model id in the second argument, such as =ASK_AI(A2, "openai/gpt-6.1-sol"). Requests run on your own ChatGPT plan, so pick an openai/ or codex/ model your plan includes, and your plan's normal usage limits apply.
Why does my ASK_AI cell show #ERROR!?
Google Sheets kills any custom function that takes longer than 30 seconds and puts #ERROR! in the cell with the note Exceeded maximum execution time (line 0). Ask for shorter replies in your prompt, or break a big range formula into smaller chunks of rows so each call finishes inside the window.
Does ASK_AI send a new request every time the sheet opens?
No, Google Sheets only recalculates a custom function when you edit the formula itself or change the value of a cell it directly references. Cells whose inputs haven't changed keep their existing text without burning new requests.
Can people I share the sheet with see my API key?
Anyone with edit access to the spreadsheet can open Extensions > Apps Script and read your PLAN2API_KEY, because script properties are shared by all users of a script. Viewers can't open the script editor, but a viewer who makes a copy of the spreadsheet owns the copy and can see the script, so use a separate p2a_ key for any sheet you share.
Sources
- Google: custom functions in Google Sheetsdevelopers.google.com
- Google: Apps Script quotasdevelopers.google.com
- Google: properties servicedevelopers.google.com
- Google: UrlFetchApp referencedevelopers.google.com