This guide walks you through building your first connector from scratch. It covers environment setup, the build workflow, common patterns, and testing strategies.
Prerequisites
Before building a connector, ensure you have:
StackOne Account & API Keys
Enterprise access to custom connectors (contact support@stackone.com )
API key with Connectors scopes enabled:
connectors:read - download connectors from registry
connectors:write - push and delete connectors
credentials:read - use linked account credentials with stackone run
See API Keys to generate one.
Most providers offer sandbox or developer accounts for testing:
Check the provider’s developer portal for sandbox signup
Generate API credentials (API key, OAuth app, etc.)
Note any rate limits or restrictions on sandbox accounts
Keep sandbox credentials separate from production. Store them in a credentials.json file that’s gitignored.
Node.js 18+ installed
StackOne CLI: npm install -g @stackone/cli
Git for version control
Claude Code, Cursor, or VS Code with Claude extension (for AI-assisted development)
Setup
Set Up Your Repository
You can build connectors in any repository using stackone pull to fetch existing connectors. However, forking the connectors-template is recommended because it includes:
Agent skills for guided connector building (including /on-boarding)
CI/CD workflows for automated deployment
Pre-configured CLAUDE.md with agent instructions
Example connector structures
git clone https://github.com/StackOneHQ/connectors-template.git my-connectors
cd my-connectors
npm install
If you’re not using the template, you can still pull connectors into any repo: stackone pull --connector provider_name --output ./connectors/
Configure the CLI
# Authenticate globally (stores credentials in ~/.stackone)
stackone agent setup --global
# Or scope to current project only
stackone agent setup --local
This configures MCP tools for AI-assisted development and stores your access token.
Create a CLI Profile (Optional)
For deployment, create a named profile: You’ll be prompted for a profile name and API key. Use different profiles for staging vs production.
Starting the Build
If you’re using the connectors-template with Claude Code or a similar AI assistant, the easiest way to start is with the onboarding command:
This launches an interactive flow that guides you through:
Connector type selection — Agentic Actions (raw provider data) vs Schema-Based (unified output)
Provider details — Name, API version, and pulling any existing connector
Authentication setup — Configuring and validating auth before building actions
Schema definition (for unified connectors) — Defining your target output format
Endpoint research — Discovering available API endpoints and their trade-offs
Action building — Implementing and testing each action
The onboarding flow ensures you complete each step before moving to the next, reducing errors and rework.
Experienced users: Skip onboarding and tell the agent directly what you need:
“Build a connector for [Provider] with employee management actions”
“Start unified build for [Provider]” (for schema-based connectors)
Build Workflow
The recommended workflow ensures you validate each step before moving to the next:
Step 1: Fork or Pull Existing Connector
Always start by checking if a connector already exists:
# Pull existing connector from StackOne registry
stackone pull --connector provider_name --output ./src/configs/
# Check what was downloaded
ls src/configs/provider_name/
If the pull succeeds, you have a foundation to build on. If it fails, create a new connector structure:
src/configs/provider-name/
├── provider-name.connector.s1.yaml # Main file (auth, metadata)
└── provider-name.employees.s1.partial.yaml # Actions by resource
Step 2: Build Authentication
Do not proceed until authentication works. All other work depends on valid auth.
Configure authentication in the main connector file:
API Key (Bearer)
Basic Auth
authentication :
- custom :
type : custom
label : API Key
authorization :
type : bearer
token : $.credentials.apiKey
configFields :
- key : apiKey
label : API Key
type : password
required : true
secret : true
testActionsIds :
- list_users # Simple action to test auth
authentication :
- custom :
type : custom
label : Basic Auth
authorization :
type : basic
username : $.credentials.username
password : $.credentials.password
configFields :
- key : username
label : Username
type : text
required : true
- key : password
label : Password
type : password
required : true
secret : true
Create a simple test action (e.g., list_users) and verify auth works before building more actions.
Step 3: Connect Account & Test Auth
Create test files for local development:
# Create account config
echo '{"environment": "production", "provider": "provider_name"}' > account.json
# Create credentials (gitignore this!)
echo '{"apiKey": "your_sandbox_api_key"}' > credentials.json
Test authentication:
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account account.json \
--credentials credentials.json \
--action-id list_users \
--debug
Alternative: Push and link account. If you prefer, push the connector and create a linked account in the StackOne dashboard. Then use --account-id instead of local credential files:stackone push src/configs/provider-name/ --profile dev
# Create linked account in dashboard, then:
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account-id your-linked-account-id \
--action-id list_users
Step 4: Build Actions
With auth working, build out your actions. See Common Step Functions for patterns.
Step 5: Iterate
Validate syntax: stackone validate src/configs/provider-name/
Test actions: stackone run --debug ...
Fix issues and repeat
Common Step Functions
request - Single HTTP Request
Use for GET single resource, POST create, PUT update operations:
steps :
- stepId : get_user
stepFunction :
functionName : request
parameters :
url : /users/${inputs.id}
method : get
response :
collection : false
dataKey : data.user # Extract nested response
Use for list endpoints with cursor-based pagination:
steps :
- stepId : list_users
stepFunction :
functionName : paginated_request
parameters :
url : /users
method : get
response :
dataKey : data.users # Path to data array
nextKey : meta.next_cursor # Path to pagination cursor
indexField : id # Unique ID field in each record
iterator :
key : cursor # Parameter name API expects
in : query # Send cursor as query param
args :
- name : limit
value : "100"
in : query
Verify paths with --debug before assuming response structure. Common mistake: using dataKey: users when the actual path is dataKey: data.users.
Transform provider response to your schema:
steps :
- stepId : map_data
stepFunction :
functionName : map_fields
version : "2"
parameters :
dataSource : $.steps.get_users.output.data
fields :
- targetFieldKey : id
expression : $.user_id
type : string
- targetFieldKey : email
expression : $.email_address
type : string
- targetFieldKey : full_name
expression : "{{$.first_name + ' ' + $.last_name}}"
type : string
- targetFieldKey : status
expression : $.account_status
type : enum
enumMapper :
matcher :
- matchExpression : '{{$.account_status == "ACTIVE"}}'
value : active
- matchExpression : '{{$.account_status == "INACTIVE"}}'
value : inactive
Always use version: "2" for map_fields and typecast steps.
typecast - Apply Type Conversions
Apply after map_fields to ensure correct types:
steps :
- stepId : typecast_data
stepFunction :
functionName : typecast
version : "2"
parameters :
dataSource : $.steps.map_data.output.data
fields :
- targetFieldKey : id
type : string
- targetFieldKey : created_at
type : datetime_string
- targetFieldKey : is_active
type : boolean
For list actions where callers can specify page size, use the dual-condition pattern:
inputs :
- name : page_size
type : number
in : query
required : false
description : Results per page (max 100)
- name : cursor
type : string
in : query
required : false
description : Pagination cursor
cursor :
enabled : true
pageSize : 50
steps :
- stepId : get_data
stepFunction :
functionName : request
parameters :
url : /items
method : get
args :
# Use page_size if provided
- name : limit
value : $.inputs.page_size
in : query
condition : "{{present(inputs.page_size)}}"
# Otherwise use default
- name : limit
value : 50
in : query
condition : "{{!present(inputs.page_size)}}"
# Pass cursor when present
- name : cursor
value : $.inputs.cursor
in : query
condition : "{{present(inputs.cursor)}}"
result :
data : $.steps.get_data.output.data.items
next : $.steps.get_data.output.data.meta.next_cursor
CLI Commands Reference
Validation
# Validate single connector
stackone validate src/configs/provider-name/provider-name.connector.s1.yaml
# Validate directory (all connectors)
stackone validate src/configs/provider-name/
# Watch mode for development
stackone validate src/configs/provider-name/ --watch
Testing Actions
# Basic test with debug output
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account account.json \
--credentials credentials.json \
--action-id list_employees \
--debug
# Test specific action with parameters
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account-id linked-account-id \
--action-id get_employee \
--params '{"id": "emp_123"}'
# Test with query parameters
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account-id linked-account-id \
--action-id list_employees \
--params '{"queryParams": {"status": "active", "limit": "10"}}'
# Test pagination
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account-id linked-account-id \
--action-id list_employees \
--params '{"page_size": 5}'
The --params flag accepts JSON with these fields:
{
"path" : {
"id" : "emp_123"
},
"queryParams" : {
"status" : "active" ,
"page_size" : "10"
},
"header" : {
"X-Custom-Header" : "value"
},
"body" : {
"name" : "John Doe"
}
}
You can also pass input parameters directly:
{
"employee_id" : "emp_123" ,
"include_details" : true
}
Deployment
# Push connector to registry
stackone push src/configs/provider-name/ --profile prod
# Pull latest version
stackone pull --connector provider_name --output ./src/configs/
# Get connector config (useful for debugging)
stackone get --connector provider_name --format yaml
Debugging
Enable Debug Mode
Add --debug to any stackone run command to see:
Raw HTTP requests sent to the provider
Full response bodies
Step-by-step execution details
Expression evaluation results
stackone run \
--connector src/configs/provider-name/provider-name.connector.s1.yaml \
--account-id linked-account-id \
--action-id list_employees \
--debug
Debug Empty or Incorrect Results
When mapping produces unexpected results:
Test raw response first
Temporarily modify your result block to return raw data: result :
data : $.steps.get_employees.output.data # Raw, before map_fields
Verify response structure
Run with --debug and examine the actual paths: stackone run --debug ... | jq '.steps.get_employees.output'
Add fields incrementally
Start with one field in map_fields, verify it works, then add more.
Check expression context
For inline fields in map_fields, expressions reference fields directly ($.email), not with step prefix ($.get_employees.email).
Common Issues
Symptom Likely Cause Fix 401 Unauthorized Invalid credentials Check API key, regenerate if needed Empty data array Wrong dataKey path Use --debug to verify actual response structure Null field values Incorrect expression path Verify nested paths match actual response Pagination returns same data Wrong iterator.key Check API docs for expected cursor parameter name Enum not translated matchExpression case mismatchProvider might return "ACTIVE" not "Active"
Error Mapping
Map provider-specific errors to meaningful responses using error handlers:
errorHandlers :
- condition : '{{response.status == 404}}'
message : "Resource not found"
code : NOT_FOUND
- condition : '{{response.status == 429}}'
message : "Rate limit exceeded. Retry after {{response.headers.retry-after}} seconds"
code : RATE_LIMITED
- condition : '{{response.status == 403}}'
message : "Insufficient permissions for this operation"
code : FORBIDDEN
For unified connectors, map provider error codes to standard error responses:
errorHandlers :
- condition : '{{response.body.error.code == "INVALID_TOKEN"}}'
message : "Authentication failed - token may be expired"
code : UNAUTHORIZED
- condition : '{{response.body.error.code == "MISSING_SCOPE"}}'
message : "Missing required permission: {{response.body.error.required_scope}}"
code : FORBIDDEN
Manual Testing Methods
Beyond stackone run, you have additional testing options:
Action Request Tester (Dashboard)
The StackOne dashboard includes an Action Request Tester for testing deployed connectors:
Navigate to your project in the dashboard
Select a linked account
Choose an action and provide parameters
Execute and view results
This tests the deployed connector version against real credentials.
RPC Calls via API
Test actions programmatically using the StackOne API:
curl -X POST "https://api.stackone.com/unified/actions/execute" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "x-account-id: your-linked-account-id" \
-d '{
"action_id": "provider_list_employees",
"params": {
"page_size": 10
}
}'
MCP Testing (AI Agents)
If using Claude Code or similar, test actions conversationally:
Test the list_employees action for the provider connector.
Use page_size of 5 and verify the response includes id, name, and email fields.
The AI agent will execute the action and validate results.
Unified Connector Checklist
For schema-based connectors, verify:
The connector building agent starts with baseline capabilities. As you build connectors, you can improve agent performance by creating custom skills that encode your specific use cases and schemas.
The Optimization Workflow
Why Create Custom Skills?
Without Skills With Skills Agent asks for schema every time Agent auto-loads your schema skill Generic action structures Actions match your specific requirements Manual field mapping decisions Consistent mapping patterns across connectors Repeated explanations of your use case Agent understands context immediately
Creating Schema Skills
After building your first few connectors, extract your schema into a reusable skill:
.claude/skills/schemas/
└── my-use-case-schema.md
Example schema skill:
---
name : Employee Sync Schema
description : Target schema for employee data synchronization
category : hris
---
# Employee Sync Schema
## Target Schema
| Field | Type | Required | Notes |
|-------|------|----------|-------|
| id | string | yes | Unique identifier |
| email | string | yes | Primary email |
| first_name | string | yes | |
| last_name | string | yes | |
| status | enum | yes | Values: active, inactive, terminated |
| department | string | no | |
| hire_date | datetime_string | no | ISO 8601 format |
## Enum Mappings
### status
| Provider Value | Schema Value |
|----------------|--------------|
| Active, active, ACTIVE | active |
| Inactive, inactive, INACTIVE | inactive |
| Terminated, terminated, TERMINATED | terminated |
| * (default) | unknown |
Once created, the agent automatically uses this schema for future unified builds — no questions needed.
Recommended Progression
Build 2-3 Connectors
Use the baseline agent to build your first connectors. Note patterns in your requirements.
Create Schema Skills
Extract your target schemas into .claude/skills/schemas/. Include field definitions, types, and enum mappings.
Document Use-Case Patterns
Create skills that describe your specific integration patterns, endpoint preferences, or data transformation rules.
Iterate and Refine
As you build more connectors, update skills based on edge cases and lessons learned.
Skills are stored in .claude/skills/ in the connectors-template repository. The agent reads these automatically when relevant to the current task.
Next Steps