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.

Values the ASK_AI script uses
SettingValue
Endpointhttps://api.plan2api.com/v1/chat/completions
Auth headerAuthorization: Bearer p2a_...
Script PropertyPLAN2API_KEY set to your p2a_ key
Default modelclaude/claude-sonnet-5
Formula=ASK_AI(A2) or =ASK_AI(A2, "kimi/k3")
Time limit30 seconds per formula call

Add =ASK_AI() to a spreadsheet

  1. Sign up at Plan2API with a Google account, connect a Claude, ChatGPT, Google AI or Kimi subscription, and copy your p2a_ key.
  2. Open your spreadsheet and select Extensions > Apps Script from the top menu.
  3. Delete any code that's already in the editor, paste in the script from the next section, and click Save at the top.
  4. On the left sidebar, click Project Settings, scroll down to Script Properties, and click Add script property.
  5. Enter PLAN2API_KEY as the Property and paste your p2a_ key as the Value, then click Save script properties.
  6. Optionally, run testPlan2API from 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.
  7. Back in the sheet, type =ASK_AI("Write a tagline for a bakery") or =ASK_AI(A2) into any cell; it'll show Loading... 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.

Apps Script
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.

Plan2API model ids for ASK_AI
Model idSubscriptionContext window
claude/claude-sonnet-5Claude Pro or Max1M tokens
claude/claude-haiku-4-5Claude Pro or Max200K tokens
openai/gpt-6.1-solChatGPT plan (Codex)Not published
gemini/gemini-3.8-flash-highGoogle AI Pro or Ultra1,048,576 tokens
kimi/k3Kimi Code membership1,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() and RAND() aren't allowed inside custom function arguments, and any cell that depends on them will sit on Loading... forever.
  • Any execution that runs past 30 seconds fails with #ERROR! and the cell note Exceeded 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_format with 400 unsupported_parameter if 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.

ASK_AI errors and fixes
Cell showsCauseFix
Add PLAN2API_KEY in Project SettingsScript Property missing or misnamedAdd PLAN2API_KEY exactly
Error 401 invalid_api_keyWrong or partial keyPaste the full p2a_ key again
Error 403 no_credentialsNo subscription for that providerConnect it or change the model
Error 400 model_not_foundTypo in the model idCopy an id from GET /v1/models
Error 429 daily_requestsFree plan's 50 requests usedWait for 00:00 UTC or upgrade
Skipped: 30-second limit reachedRange too big for one callSplit the range
#ERROR! Exceeded maximum execution timeOne reply took over 30 secondsAsk for a shorter answer
Loading... that never endsNOW() or RAND() in the argumentsReference 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

  1. Google: custom functions in Google Sheetsdevelopers.google.com
  2. Google: Apps Script quotasdevelopers.google.com
  3. Google: properties servicedevelopers.google.com
  4. Google: UrlFetchApp referencedevelopers.google.com