Get container-bound script ID from container ID

Example: I have a spreadsheet and wish to look up its bound script with code. Is this relationship programatically available for files I own?

If you need to be 100% accurate or handle files with multiple script versions, you should use the REST API. You cannot do this with the basic SpreadsheetApp service; you must enable the Apps Script API in your Google Cloud Project.

The Logic: You can query for projects where the parentId matches your spreadsheet ID.

function getBoundScriptId(spreadsheetId) {
const url = https://script.googleapis.com/v1/projects?parentId=${spreadsheetId};
const options = {
headers: { Authorization: 'Bearer ’ + ScriptApp.getOAuthToken() },
muteHttpExceptions: true
};

const response = UrlFetchApp.fetch(url, options);
const data = JSON.parse(response.getContentText());

return data.projects ? data.projects[0].scriptId : “No bound script found”;
}

I tried following your recommendation. But I am getting 400 bad request and some html page is getting returned. I want to get the scriptId. I wish to call this API from postman also.

Dear @brettfreer.

Apologies for not getting back sooner. Trying to use the API this way returns an html 404 page so something’s not working this way round. Gemini has suggested the same thing as well as using the Drive advanced service to perform the lookup to the parent. None of them work. Has something changed and are you able to use this piece of code you shared consistently these days?

* Finds the Script ID bound to a specific Google Sheet/Doc ID
* @param {string} containerId The ID of the Spreadsheet, Doc, etc.
* @return {string[]} An array of found Script IDs
*/
function getBoundScriptId(containerId) {
  // We search for files with the script mimeType 
  // and check if the containerId is in the 'parents' collection
  const query = `mimeType = 'application/vnd.google-apps.script' and '${containerId}' in parents`;

  try {
    const files = Drive.Files.list({
      q: query,
      supportsAllDrives: true,
      includeItemsFromTrash: false
    });

    console.log(files);

    if (files.items && files.items.length > 0) {
      return files.items.map(file => ({
        name: file.title,
        id: file.id
      }));

    } else {
      return "No bound scripts found for this container.";
    }
  } catch (e) {
    return "Error: " + e.toString();
  }
}

OK. I am stumped. I have tried everything I can think of today to get this working. Sorry. :pensive_face:

Research Summary: The Impossibility of External Container-to-Bound-Script Resolution

Objective
To determine if a container-bound Google Apps Script ID can be programmatically resolved solely by possessing the Container ID (Spreadsheet/Doc/Form ID), using an external script running as a standard user.

Conclusion
It is currently impossible to resolve a bound script ID from the outside without prior internal intervention. The relationship is strictly one-way (Script knows Parent; Parent does not know Script) across all available public APIs.

Technical Configuration

  • GCP APIs Enabled: Google Drive API (v3), Google Apps Script API.

  • Advanced Services: Drive (v2/v3).

  • Auth Scopes Tested:

    • https://www.googleapis.com/auth/drive.readonly

    • https://www.googleapis.com/auth/script.projects.readonly

    • https://www.googleapis.com/auth/script.processes

    • https://www.googleapis.com/auth/spreadsheets

The “Rabbit Holes” & Findings

1. Drive API: The “Child” Hypothesis (Drive.Files.list)

  • Hypothesis: Bound scripts are “children” of the container in the file system.

  • Method: Querying files.list with q parameters for application/vnd.google-apps.script and 'containerId' in parents.

  • Result: Failed. Bound scripts return an empty parents array in the Drive API. They do not exist in the folder hierarchy.

2. Drive Metadata: The “Hidden Property” Hypothesis (Drive.Files.get)

  • Hypothesis: The container stores the script ID in hidden metadata fields (appProperties, properties, exportLinks).

  • Method: Using fields: '*' to retrieve the raw JSON resource of the container file.

  • Result: Failed. The container is completely agnostic of the script. No scriptId or linkId exists in the container’s metadata, permissions, or properties.

3. Global Drive Crawling: The “Ghost File” Reality

  • Hypothesis: Bound scripts exist in the global Drive index and can be found by listing all scripts and checking their internal metadata.

  • Method: Iterating through files.list for all script MIME types.

  • Result: Failed. Bound scripts are “ghosted” from the list endpoint. While they can be fetched directly if the ID is known (files.get), they do not appear in files.list results, rendering a brute-force search impossible.

4. Apps Script API: The “One-Way Street”

  • Method: Using projects.get on a known script ID.

  • Result: Confirmed One-Way Link. The API returns a parentId field matching the Container ID. However, the API lacks a search endpoint to query by parentId.

5. Processes API: The “Execution Pulse” Strategy

  • Hypothesis: Identify the script ID by scanning recent execution logs via processes.list.

  • Method: Listing recent executions (userProcessFilter.userAccessLevels=OWNER) for active bound scripts.

  • Result: Failed. The API returns projectName and functionName but explicitly omits the scriptId from the response object.

6. Blob Inspection

  • Hypothesis: The script code is embedded in the file blob.

  • Method: Fetching the container blob via DriveApp and inspecting the stream.

  • Result: Failed. Fetching a container results in a conversion (e.g., to PDF/XLSX), stripping all Apps Script code from the output.

Final Verdict & Recommendation

Google’s architecture creates a security wall preventing the scanning of containers for executable code.

  • External Resolution: Impossible via API discovery.

  • Working Solution (“Inside-Out”): The bound script must write its own ID to the container’s DeveloperMetadata (using addDeveloperMetadata). This creates a persistent, queryable link accessible to external scripts via the Sheets/Drive API.

…posting the dead ends I have been down with Gemini Pro these days to explore all ways I could think of to find a way to obtain a container-bound script from its container id. It seems Google deliberately is keeping this one-way for security/performance issues.

Thanks @brewguy for sharing such a detailed summary and your findings.