Common Pitfalls to Avoid When Developing OpenAI Plugins: A Complete Guide

The most frequent failures in OpenAI plugin development stem from missing manifest files at plugins/<name>/.codex-plugin/plugin.json, unawaited async calls in skills like use_figma, and neglecting to return node IDs from mutations. Successful plugins require strict adherence to the Codex runtime's expectations for state persistence, OAuth token rotation, and skill prerequisites.

Developing OpenAI plugins involves creating Codex-plugin bundles that live under plugins/<name>/ in the openai/plugins repository. Each bundle must contain a valid manifest, optional skills, and proper async handling to avoid hard-to-debug runtime failures. Understanding these common pitfalls to avoid when developing OpenAI plugins early in your workflow prevents silent failures and integration errors that are difficult to trace.

Manifest and Entry Point Configuration

Missing or Incorrectly Named Manifest Files

The Codex runtime strictly validates the manifest location. Every plugin must place its configuration exactly at plugins/<name>/.codex-plugin/plugin.json. A missing file or misnamed skills/ folder prevents the plugin from loading entirely, as the runtime checks for this file during initialization.

Directory Structure Requirements

Reference the skills folder with a trailing slash ("skills": "./skills/") in the manifest. The Zoom plugin provides a correct implementation example at plugins/zoom/.codex-plugin/plugin.json, demonstrating the required structure and path formatting.

Skill Execution and Async Handling

Prerequisite Skill Loading

Calling a skill like use_figma without first loading its prerequisite skill (figma-use) causes silent failures. The figma-use skill depends on global setup for API typings and page-state handling, as documented in plugins/figma/skills/figma-use/SKILL.md. Always load the prerequisite skill before invoking dependent operations.

Unawaited Promises and Race Conditions

The helper scripts run in an async wrapper. Omitting await on Promise-returning calls such as figma.loadFontAsync or figma.setCurrentPageAsync produces race conditions where changes may never persist. The skill documentation explicitly warns to "await every Promise" to ensure deterministic execution.

Node-ID Return Requirements

The Codex harness only receives data that a skill explicitly returns. You must always return created or mutated node IDs so subsequent calls can reference newly created objects:

{
  "tool": "use_figma",
  "skillNames": "figma-use",
  "code": "await figma.setCurrentPageAsync(page);\nconst rect = figma.createRectangle();\nrect.set({ width: 200, height: 100, fills: [{type:'SOLID',color:{r:0,g:0.5,b:0}}] });\nreturn { createdNodeIds: [rect.id] };"
}

This pattern follows the rule in plugins/figma/skills/figma-use/SKILL.md requiring you to return all created/mutated node IDs.

Page Context Management

Switching pages multiple times inside a single use_figma script triggers unnecessary reloads and can raise the error "Setting figma.currentPage is not supported". Call await figma.setCurrentPageAsync(page) once per script and split multi-page work into separate parallel use_figma calls to avoid these restrictions.

OAuth and API Integration Failures

Refresh Token Rotation

Providers like Zoom and Google rotate refresh tokens on each use, invalidating previous tokens. Assuming refresh tokens remain stable causes authentication failures after the first exchange. Implement token rotation handling as shown in plugins/zoom/skills/oauth/references/full-guide.md:

import requests, os

TOKEN_URL = "https://zoom.us/oauth/token"
CLIENT_ID = os.getenv("ZOOM_CLIENT_ID")
CLIENT_SECRET = os.getenv("ZOOM_CLIENT_SECRET")
REFRESH_TOKEN = os.getenv("ZOOM_REFRESH_TOKEN")

def refresh_access():
    resp = requests.post(
        TOKEN_URL,
        auth=(CLIENT_ID, CLIENT_SECRET),
        data={"grant_type": "refresh_token", "refresh_token": REFRESH_TOKEN},
    )
    data = resp.json()
    # Store the new refresh token

    os.environ["ZOOM_REFRESH_TOKEN"] = data["refresh_token"]
    return data["access_token"]

Redirect-URI Mismatches

Deploying a plugin with a callback URL that differs from the provider console registration causes immediate OAuth rejection before any token exchange occurs. Verify the exact URI in the provider's developer console and keep it in sync with /.app.json, as detailed in plugins/zoom/skills/oauth/troubleshooting/redirect-uri-issues.md.

Platform-Specific API Usage

Using APIs only available in one environment (e.g., figma.createPage() in FigJam) throws "not supported" errors. Detect the editor type using figma.editorType and call only supported methods, following the design-only vs Slides-only API lists in plugins/figma/skills/figma-use/SKILL.md.

Rate Limiting and Pagination

Ignoring pagination tokens or exceeding API limits results in partial data and 429 errors. Always check next_page_token and respect Retry-After headers as documented in plugins/zoom/skills/rest-api/troubleshooting/common-issues.md:

import time, requests, os

def fetch_all(endpoint):
    token = os.getenv("ZOOM_ACCESS_TOKEN")
    url = f"https://api.zoom.us/v2/{endpoint}"
    results = []
    while url:
        resp = requests.get(url, headers={"Authorization": f"Bearer {token}"})
        data = resp.json()
        results.extend(data.get("users", []))
        next_token = data.get("next_page_token")
        url = f"https://api.zoom.us/v2/{endpoint}?next_page_token={next_token}" if next_token else None
        if resp.status_code == 429:
            retry = int(resp.headers.get("Retry-After", "1"))
            time.sleep(retry)
    return results

Environment and File System Safety

File System Side Effects

Creating folders or files without checking existence causes crashes on first run or in CI environments where directories may already exist. Use os.makedirs(..., exist_ok=True) or explicit existence checks, as noted in the common pitfalls section of plugins/base44/skills/base44-cli/SKILL.md.

Environment Variables and Secrets

Hard-coding API keys or forgetting .env files causes runtime errors since secrets are stripped from the repository. Create a .env placeholder (API_KEY=) and document required variables in the README to prevent launch failures.

Deprecated Library Imports

Importing deprecated sub-modules (e.g., @wix/data instead of @wix/wix-data-items-sdk) causes build failures. Follow upgrade notes in skill documentation, such as those in plugins/wix/skills/wix-headless/references/astro/cms/CMS_FOUNDATIONS.md, to maintain compatible dependencies.

Summary

  • Place the manifest strictly at plugins/<name>/.codex-plugin/plugin.json with correct trailing slashes for skill paths.
  • Always load prerequisite skills (e.g., figma-use before use_figma) and await every Promise-returning call.
  • Return all created and mutated node IDs from skills; the Codex runtime discards console logs and side-effects.
  • Handle OAuth refresh token rotation and verify redirect URIs match provider console settings exactly.
  • Implement pagination loops with next_page_token and respect Retry-After headers for rate limits.
  • Guard file system operations with exist_ok=True and use environment variables for all secrets to avoid CI failures.

Frequently Asked Questions

What happens if I forget to return node IDs from a skill?

The Codex runtime persists only explicit return values. If you omit createdNodeIds or mutatedNodeIds from your return object, subsequent calls cannot reference those objects, causing "node not found" failures in multi-step workflows.

Why does my plugin fail to load even though the code looks correct?

The most common cause is a missing manifest at plugins/<name>/.codex-plugin/plugin.json or an incorrectly named skills/ folder. The Codex runtime validates these paths strictly during initialization, and any deviation prevents the plugin from registering.

How do I handle APIs that return paginated results?

Check for next_page_token in the response and construct subsequent requests using that token. Always implement backoff logic that respects Retry-After headers when encountering 429 status codes to avoid rate limit blocks.

Are refresh tokens stable across multiple OAuth requests?

No. Providers like Zoom and Google rotate refresh tokens on each use. You must capture and store the new refresh_token returned with each access token refresh to prevent "invalid refresh token" authentication failures.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →