# Google OAuth

Connect Google Sheets, Docs, and Drive to your workflows using one Google OAuth handle.

JsWorkflows provides a platform-managed Google OAuth app. You do not need to create your own Google Cloud project or manage Google client credentials.

## How Google OAuth works in JsWorkflows

Google OAuth in JsWorkflows is built around one idea:

- create a Google connection once
- give it a handle such as `my-google`
- use that handle later in workflow code, templates, or the editor

You can then use that same handle to:

- get an access token in workflow code with `api.getOAuthToken(handle)`
- pick Google Sheets, Docs, or Drive folders from the app UI
- save those selected file references into template settings or workflow variables

## Current Google scope

JsWorkflows currently documents the Google picker-based file flow around this scope:

| Resource | Use in JsWorkflows | Scope |
| --- | --- | --- |
| Google Drive, Google Sheets, Google Docs | Select existing files and folders, create files, upload files, and work with selected Sheets and Docs | `drive.file` |

`drive.file` is the important scope for Google Sheets, Google Docs, and Google Drive file picker flows. It allows you to select the specific files you want to share with JsWorkflows and lets workflows create new files as needed.

## Step 1: Create a Google connection

You can open the OAuth section in either of these places:

1. From **Settings → OAuth2 Tokens**
2. From the workflow code editor:
   - open **More actions**
   - select **Manage OAuth2 tokens**

Then:

1. Click **Connect to Service**
2. Choose **Google**
3. Complete Google's consent screen
4. Save the connection with a handle such as `my-google`

That handle is the stable name your workflows use later. Use lowercase letters, numbers, and hyphens only.

## Step 2: Use the handle with a file picker

You do **not** pick files during the OAuth approval step.

Instead, the normal flow is:

1. create the Google OAuth connection
2. save the handle
3. later, enter that handle in either:
   - the **Manage Environment Variables** page in the editor
   - a **template settings** field that uses the Google picker
4. select the Google Sheet, Doc, or Drive folder you want the workflow to use

This keeps the Google connection separate from the file selection.

## Using the picker in the editor

In **Manage Environment Variables**, the Google picker is an optional helper. It does not replace normal secret variables.

Typical flow:

1. open **Manage Environment Variables**
2. in **Optional: Add Google File Reference**, enter your Google OAuth handle
3. choose the resource type
4. click the picker button
5. select the file or folder
6. save the selected file reference as a workflow variable

The variable key you choose becomes the `env` key used in workflow code.

For example:

- if you save the picker value under the variable key `GOOGLE_SHEET`
- the workflow reads it as `env.GOOGLE_SHEET`

The saved value is a JSON reference, for example:

```json
{
  "id": "1abc123...",
  "name": "Inventory Sheet",
  "mimeType": "application/vnd.google-apps.spreadsheet",
  "resourceKey": "",
  "url": "https://docs.google.com/spreadsheets/d/1abc123..."
}
```

This is workflow-scoped. It is not a store-global file setting.

So a common pattern is:

1. save the selected spreadsheet reference under a key such as `GOOGLE_SHEET`
2. read that value later in code with `env.GOOGLE_SHEET`

## Using the picker in templates

Templates can also use Google picker fields.

In a picker-enabled template:

1. enter the Google OAuth handle in the template settings
2. use the file picker field for the spreadsheet, document, or folder
3. save the workflow

When the workflow is saved, JsWorkflows generates the runtime `config.js` for that workflow using the selected template values.

The original template file is not modified. The saved file references belong to the workflow created from the template.

## Using the saved file reference in code

The safest pattern is to support either:

- a raw Google file ID
- or a JSON reference saved by the picker

Use a small helper:

```js
function parseGoogleReference(input) {
  const raw = String(input || '').trim();
  if (!raw) return { id: '', resourceKey: '' };

  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return {
        id: String(parsed.id || '').trim(),
        resourceKey: String(parsed.resourceKey || '').trim(),
      };
    }
  } catch {}

  return { id: raw, resourceKey: '' };
}
```

## Example: Google Sheets

This example appends one row to a spreadsheet selected with the picker.

```js
function parseGoogleReference(input) {
  const raw = String(input || '').trim();
  if (!raw) return { id: '', resourceKey: '' };

  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return {
        id: String(parsed.id || '').trim(),
        resourceKey: String(parsed.resourceKey || '').trim(),
      };
    }
  } catch {}

  return { id: raw, resourceKey: '' };
}

export class Workflow {
  async start(payload, headers, api) {
    const { token, error } = await api.getOAuthToken('my-google');
    if (error || !token) throw new Error(error || 'Missing Google token');

    const { id: spreadsheetId, resourceKey } = parseGoogleReference(env.SHEET_REF);
    const range = encodeURIComponent('Sheet1!A1');

    const requestHeaders = {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    };

    if (resourceKey) {
      requestHeaders['X-Goog-Drive-Resource-Keys'] = `${spreadsheetId}/${resourceKey}`;
    }

    const response = await fetch(
      `https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${range}:append?valueInputOption=USER_ENTERED`,
      {
        method: 'POST',
        headers: requestHeaders,
        body: JSON.stringify({
          values: [[
            'JsWorkflows',
            'Google Sheets test',
            new Date().toISOString(),
          ]],
        }),
      }
    );

    if (!response.ok) {
      throw new Error(`Google Sheets append failed: ${response.status} ${await response.text()}`);
    }
  }
}
```

If your sheet name contains spaces or special characters, always URL-encode the full range string before placing it in the Sheets API URL.

## Example: Google Docs

This example appends text to a Google Doc selected with the picker.

```js
function parseGoogleReference(input) {
  const raw = String(input || '').trim();
  if (!raw) return { id: '', resourceKey: '' };

  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return {
        id: String(parsed.id || '').trim(),
        resourceKey: String(parsed.resourceKey || '').trim(),
      };
    }
  } catch {}

  return { id: raw, resourceKey: '' };
}

export class Workflow {
  async start(payload, headers, api) {
    const { token, error } = await api.getOAuthToken('my-google');
    if (error || !token) throw new Error(error || 'Missing Google token');

    const { id: documentId, resourceKey } = parseGoogleReference(env.DOC_REF);
    const requestHeaders = { Authorization: `Bearer ${token}` };

    if (resourceKey) {
      requestHeaders['X-Goog-Drive-Resource-Keys'] = `${documentId}/${resourceKey}`;
    }

    const docRes = await fetch(`https://docs.googleapis.com/v1/documents/${documentId}`, {
      headers: requestHeaders,
    });
    if (!docRes.ok) {
      throw new Error(`Google Docs read failed: ${docRes.status} ${await docRes.text()}`);
    }
    const doc = await docRes.json();
    const endIndex = Math.max(1, (doc.body?.content?.[doc.body.content.length - 1]?.endIndex ?? 1) - 1);

    const updateHeaders = {
      Authorization: `Bearer ${token}`,
      'Content-Type': 'application/json',
    };

    if (resourceKey) {
      updateHeaders['X-Goog-Drive-Resource-Keys'] = `${documentId}/${resourceKey}`;
    }

    const updateRes = await fetch(`https://docs.googleapis.com/v1/documents/${documentId}:batchUpdate`, {
      method: 'POST',
      headers: updateHeaders,
      body: JSON.stringify({
        requests: [
          {
            insertText: {
              location: { index: endIndex },
              text: `Updated by JsWorkflows at ${new Date().toISOString()}\n`,
            },
          },
        ],
      }),
    });

    if (!updateRes.ok) {
      throw new Error(`Google Docs update failed: ${updateRes.status} ${await updateRes.text()}`);
    }
  }
}
```

## Example: Google Drive folder

This example uploads a text file to a Drive folder selected with the picker.

```js
function parseGoogleReference(input) {
  const raw = String(input || '').trim();
  if (!raw) return { id: '', resourceKey: '' };

  try {
    const parsed = JSON.parse(raw);
    if (parsed && typeof parsed === 'object') {
      return {
        id: String(parsed.id || '').trim(),
        resourceKey: String(parsed.resourceKey || '').trim(),
      };
    }
  } catch {}

  return { id: raw, resourceKey: '' };
}

export class Workflow {
  async start(payload, headers, api) {
    const { token, error } = await api.getOAuthToken('my-google');
    if (error || !token) throw new Error(error || 'Missing Google token');

    const { id: folderId, resourceKey } = parseGoogleReference(env.DRIVE_FOLDER_REF);
    const boundary = `jsw-${Date.now()}`;
    const metadata = {
      name: `jsworkflows-demo-${Date.now()}.txt`,
      mimeType: 'text/plain',
      parents: folderId ? [folderId] : undefined,
    };

    const body = [
      `--${boundary}`,
      'Content-Type: application/json; charset=UTF-8',
      '',
      JSON.stringify(metadata),
      `--${boundary}`,
      'Content-Type: text/plain; charset=UTF-8',
      '',
      'Created by JsWorkflows',
      `--${boundary}--`,
      '',
    ].join('\r\n');

    const requestHeaders = {
      Authorization: `Bearer ${token}`,
      'Content-Type': `multipart/related; boundary=${boundary}`,
    };

    if (folderId && resourceKey) {
      requestHeaders['X-Goog-Drive-Resource-Keys'] = `${folderId}/${resourceKey}`;
    }

    const response = await fetch('https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart', {
      method: 'POST',
      headers: requestHeaders,
      body,
    });

    if (!response.ok) {
      throw new Error(`Google Drive upload failed: ${response.status} ${await response.text()}`);
    }
  }
}
```

## Picker values vs pasted file IDs

In JsWorkflows, a Google file reference can come from two places:

- the built-in picker
- a manually pasted Google file ID

### Use the picker when

- you want to choose an existing Google Sheet, Doc, or Drive folder from the app UI
- you want the workflow to use the file you selected through your Google connection
- you want the safest setup for `drive.file`

This is the recommended option for most merchants.

### Use a pasted file ID only when

- you already know the exact Google file ID
- you copied it from a Google URL
- the file is already accessible to the same Google OAuth connection
- you are updating an existing workflow that already has access to that file

A Google file ID is the long identifier inside a Google URL. For example:

```text
https://docs.google.com/spreadsheets/d/1abc123XYZ456/edit
                                   ^^^^^^^^^^^^^
                                   this part is the file ID
```

Knowing a Google file ID does **not** automatically give the workflow access to that file.

With `drive.file`, a workflow can only use files that are already accessible to the same Google OAuth connection. In practice, that usually means:

- the file was selected with the picker
- the file was created by the workflow
- or the file was already made accessible to that same Google OAuth app and connection

If you are unsure, use the picker. That is the clearest and safest way to make an existing Google Sheet, Doc, or Drive folder available to the workflow.

For most merchants, the picker should be treated as the normal way to connect an existing Google file to a workflow.

Manual file IDs are an advanced option. They do not grant access by themselves.

## Summary

The normal Google OAuth pattern in JsWorkflows is:

1. create one Google OAuth connection
2. save a handle
3. use that handle in workflow code with `api.getOAuthToken(handle)`
4. optionally use the picker in:
   - template settings
   - Manage Environment Variables
5. save the selected file reference and use it in code

> Note: JsWorkflows' use of Google API data complies with the <a href="https://developers.google.com/terms/api-services-user-data-policy" target="_blank" rel="noreferrer">Google API Services User Data Policy</a>, including Limited Use requirements.