Skip to content

Your First Connector

Connectors are tools that integrate with OAuth services. They let PIE access your accounts (Gmail, Notion, etc.) securely.

What Makes Connectors Special

  • OAuth flow handled by PIE - You don't write OAuth code
  • Tokens never exposed - Your code uses context.oauth.fetch()
  • Auto-refresh - PIE refreshes tokens automatically
  • Secure storage - Tokens encrypted with AES-256-GCM

Example: Notion Connector

Let's build a connector that searches your Notion workspace.

Step 1: Create a Notion Integration

  1. Go to Notion Integrations
  2. Click New Integration
  3. Name it "PIE Connector"
  4. Enable Read content capability
  5. Save and copy the OAuth client ID and OAuth client secret
  6. Add https://your-pie-domain.com/api/oauth/plugin/{pluginId}/callback as a redirect URI

Step 2: Create the Agent

Tier: Connector

Code:

js
/**
 * Notion Connector - Search and read Notion pages
 * 
 * Uses context.oauth.fetch() - PIE handles tokens securely.
 * This code NEVER sees the access token.
 */

const NOTION_API = 'https://api.notion.com/v1';

async function handler(input, context) {
  const { action, query } = input;
  
  // Check if OAuth is connected
  const isConnected = await context.oauth.isConnected();
  if (!isConnected) {
    return { 
      error: true, 
      message: 'Notion not connected. Please connect in Agents.',
      requiresAuth: true
    };
  }
  
  try {
    switch (action) {
      case 'search': {
        if (!query) {
          return { error: true, message: 'Search query is required' };
        }
        
        // PIE automatically adds the Bearer token
        const response = await context.oauth.fetch(
          `${NOTION_API}/search`,
          {
            method: 'POST',
            headers: {
              'Content-Type': 'application/json',
              'Notion-Version': '2022-06-28',
            },
            body: JSON.stringify({
              query,
              page_size: 10,
            }),
          }
        );
        
        if (!response.ok) {
          throw new Error(`Notion search failed: ${response.status}`);
        }
        
        const data = JSON.parse(response.body);
        
        // Format results for the AI
        const results = data.results.map(page => ({
          id: page.id,
          title: page.properties?.title?.title?.[0]?.plain_text || 'Untitled',
          type: page.object,
          url: page.url,
        }));
        
        return { 
          success: true, 
          action: 'search',
          query,
          count: results.length,
          results 
        };
      }
      
      default:
        return { 
          error: true, 
          message: `Unknown action: ${action}. Valid actions: search` 
        };
    }
  } catch (error) {
    return { 
      error: true, 
      message: error.message || 'Notion operation failed'
    };
  }
}

module.exports = { handler };

Manifest:

json
{
  "trigger": "auto",
  "oauth": {
    "provider": "notion",
    "scopes": [],
    "clientIdSecret": "NOTION_CLIENT_ID",
    "clientSecretSecret": "NOTION_CLIENT_SECRET"
  },
  "developerSecrets": {
    "NOTION_CLIENT_ID": {
      "description": "Notion OAuth Client ID",
      "required": true
    },
    "NOTION_CLIENT_SECRET": {
      "description": "Notion OAuth Client Secret",
      "required": true
    }
  },
  "tool": {
    "name": "notion",
    "description": "Search and read Notion pages. Use when the user asks about their Notion workspace.",
    "parameters": {
      "type": "object",
      "properties": {
        "action": {
          "type": "string",
          "enum": ["search"],
          "description": "The Notion action to perform."
        },
        "query": {
          "type": "string",
          "description": "For search: text to search for in Notion."
        }
      },
      "required": ["action"]
    }
  }
}

Note: Notion is a built-in provider, but you can also configure it as a custom provider if needed. See the Custom Providers section below for an example of custom OAuth configuration.

Step 3: Add OAuth Credentials

After creating the agent, add your Notion OAuth credentials as developer secrets.

OAuth Manifest Schema

Built-in Providers

For Google, GitHub, Slack, Notion - use the provider name:

json
{
  "oauth": {
    "provider": "google",
    "scopes": ["https://www.googleapis.com/auth/gmail.readonly"],
    "clientIdSecret": "GOOGLE_CLIENT_ID",
    "clientSecretSecret": "GOOGLE_CLIENT_SECRET"
  }
}

Custom Providers

For services without a built-in provider, use "provider": "custom":

json
{
  "oauth": {
    "provider": "custom",
    "providerName": "Dropbox",
    "authorizationUrl": "https://www.dropbox.com/oauth2/authorize",
    "tokenUrl": "https://api.dropboxapi.com/oauth2/token",
    "scopes": ["files.metadata.read", "files.content.read"],
    "clientIdSecret": "DROPBOX_CLIENT_ID",
    "clientSecretSecret": "DROPBOX_CLIENT_SECRET"
  }
}

The OAuth Context API

MethodDescription
context.oauth.isConnected()Check if user has connected
context.oauth.fetch(url, options)Make authenticated request
context.oauth.getConnectionInfo()Get email, provider info

context.oauth.fetch()

This is the key method. It works like regular fetch but PIE automatically:

  1. Adds the Authorization: Bearer {token} header
  2. Refreshes the token if expired
  3. Handles errors gracefully
js
const response = await context.oauth.fetch(
  'https://api.example.com/data',
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key: 'value' }),
  }
);

// response = { ok: boolean, status: number, body: string, headers: object }

Security: Why Tokens Are Never Exposed

Your agent code runs in an isolated sandbox (E2B sandbox or isolated-vm). When you call context.oauth.fetch():

  1. Your code sends the request details to PIE
  2. PIE (outside the sandbox) adds the OAuth token
  3. PIE makes the actual HTTP request
  4. PIE returns the response to your sandbox

The token exists only in PIE's trusted code, never in your agent.

Next Steps

Built with VitePress