Skip to main content
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:
  • 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

1

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/
2

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.
3

Create a CLI Profile (Optional)

For deployment, create a named profile:
stackone init
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:
/on-boarding
This launches an interactive flow that guides you through:
  1. Connector type selection — Agentic Actions (raw provider data) vs Schema-Based (unified output)
  2. Provider details — Name, API version, and pulling any existing connector
  3. Authentication setup — Configuring and validating auth before building actions
  4. Schema definition (for unified connectors) — Defining your target output format
  5. Endpoint research — Discovering available API endpoints and their trade-offs
  6. 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:
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
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

  1. Validate syntax: stackone validate src/configs/provider-name/
  2. Test actions: stackone run --debug ...
  3. 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

paginated_request - Cursor Pagination

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.

map_fields - Transform Data (Unified Connectors)

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

Cursor Pagination with Dynamic Page Size

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}'

Parameter Format

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:
1

Test raw response first

Temporarily modify your result block to return raw data:
result:
  data: $.steps.get_employees.output.data  # Raw, before map_fields
2

Verify response structure

Run with --debug and examine the actual paths:
stackone run --debug ... | jq '.steps.get_employees.output'
3

Add fields incrementally

Start with one field in map_fields, verify it works, then add more.
4

Check expression context

For inline fields in map_fields, expressions reference fields directly ($.email), not with step prefix ($.get_employees.email).

Common Issues

SymptomLikely CauseFix
401 UnauthorizedInvalid credentialsCheck API key, regenerate if needed
Empty data arrayWrong dataKey pathUse --debug to verify actual response structure
Null field valuesIncorrect expression pathVerify nested paths match actual response
Pagination returns same dataWrong iterator.keyCheck API docs for expected cursor parameter name
Enum not translatedmatchExpression 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:
  1. Navigate to your project in the dashboard
  2. Select a linked account
  3. Choose an action and provide parameters
  4. 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:
  • Target schema documented before building
  • All required fields identified
  • Field types specified (string, number, enum, datetime_string)
  • Enum values defined with mappings
  • fieldConfigs map all schema fields
  • targetFieldKey uses YOUR schema names (not provider names)
  • Nested paths verified against actual response
  • Enum mappings handle all provider values + default case
  • map_fields step with version: "2"
  • typecast step with version: "2"
  • Correct dataSource references between steps

Optimizing Agent Performance

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 SkillsWith Skills
Agent asks for schema every timeAgent auto-loads your schema skill
Generic action structuresActions match your specific requirements
Manual field mapping decisionsConsistent mapping patterns across connectors
Repeated explanations of your use caseAgent 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.
1

Build 2-3 Connectors

Use the baseline agent to build your first connectors. Note patterns in your requirements.
2

Create Schema Skills

Extract your target schemas into .claude/skills/schemas/. Include field definitions, types, and enum mappings.
3

Document Use-Case Patterns

Create skills that describe your specific integration patterns, endpoint preferences, or data transformation rules.
4

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