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
- Go to Notion Integrations
- Click New Integration
- Name it "PIE Connector"
- Enable Read content capability
- Save and copy the OAuth client ID and OAuth client secret
- Add
https://your-pie-domain.com/api/oauth/plugin/{pluginId}/callbackas a redirect URI
Step 2: Create the Agent
Tier: Connector
Code:
/**
* 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:
{
"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:
{
"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":
{
"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
| Method | Description |
|---|---|
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:
- Adds the
Authorization: Bearer {token}header - Refreshes the token if expired
- Handles errors gracefully
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():
- Your code sends the request details to PIE
- PIE (outside the sandbox) adds the OAuth token
- PIE makes the actual HTTP request
- PIE returns the response to your sandbox
The token exists only in PIE's trusted code, never in your agent.
Next Steps
- OAuth Providers Reference - All supported providers
- Gmail Connector Example - Full featured connector
- Error Handling Guide - Handle OAuth errors gracefully