ResourceSyncer contract. The interface is focused: four methods per resource type, and the SDK handles everything else.
Your connector answers three questions:
- What exists? Users, groups, roles, applications
- What permissions are available? Entitlements that can be granted
- Who has what? Grants connecting users to permissions
Project structure
Directory layout
A common structure for Baton connectors:Not all connectors follow this exact structure. Some organize code differently based on their needs. The structure above is a common starting point, not a requirement.
baton-{service} - for example, baton-github, baton-okta, baton-salesforce.
Key files
| File | Purpose |
|---|---|
cmd/.../main.go | Entry point. Parses config, creates connector, runs CLI |
pkg/connector/connector.go | Registers all resource builders with the SDK |
pkg/connector/*.go | One file per resource type implementing ResourceSyncer |
pkg/client/client.go | Wraps target system API with Go methods |
baton_capabilities.json | Declares what operations (sync, grant, revoke) are supported |
Makefile targets
Standard connectors include these make targets:Setting up a new connector
1
Create repository
2
Add baton-sdk dependency
3
Create directory structure
4
Copy standard files
From an existing connector:
.golangci.yml(lint configuration)Makefile(build targets).github/workflows/ci.yaml(CI workflow).github/workflows/release.yaml(release workflow)
5
Implement the connector
Following the patterns in this guide
Capability manifest
The capability manifest declares what operations your connector supports. This file is auto-generated by running:| Capability | Meaning |
|---|---|
CAPABILITY_SYNC | Resource type participates in sync |
CAPABILITY_TARGETED_SYNC | Supports fetching specific resources by ID |
CAPABILITY_PROVISION | Supports Grant/Revoke operations |
Implementing ResourceSyncer
TheResourceSyncer interface is the heart of your connector. Per resource type, implement four methods:
ResourceType(ctx) *v2.ResourceTypeList(ctx, parentResourceID, token) ([]*v2.Resource, nextToken, annotations, error)Entitlements(ctx, resource, token) ([]*v2.Entitlement, nextToken, annotations, error)Grants(ctx, resource, token) ([]*v2.Grant, nextToken, annotations, error)
ResourceType()
Defines what this resource is:TRAIT_USER for people, TRAIT_GROUP for collections, TRAIT_ROLE for permission bundles.
List()
Fetches all instances of this resource type:The RawId annotation
Always include aRawId annotation with the external system’s stable identifier:
RawId to:
- Correlate resources across syncs - Same ID = same resource, not a duplicate
- Track provenance - Know which connector discovered which resource
- Enable pre-sync patterns - Support reservation mechanisms that create placeholders before sync
| System | RawId value | Example |
|---|---|---|
| Okta | app.Id | 0oa1xyz789abcdef0 |
| AWS | ARN | arn:aws:iam::123456789:user/alice |
| GCP | Resource name | projects/my-project-123 |
| Azure AD | Object ID | 550e8400-e29b-41d4-a716-446655440000 |
| GitHub | Node ID or numeric ID | MDQ6VXNlcjE= or 12345 |
Entitlements()
Defines what permissions this resource offers:Grants()
Reports who has each entitlement:Pagination must progress: the SDK detects and errors if your “next page token” repeats the input token.
Modeling decisions
How you structure resources and entitlements determines what ConductorOne can manage.What to sync as a resource?
| Good candidates | Why |
|---|---|
| Users | People who have access |
| Groups | Collections that grant access |
| Roles | Permission bundles |
| Teams | Organizational units with permissions |
| Projects/Workspaces | Scoped containers |
| Skip these | Why |
|---|---|
| Business data | Customers, orders, tickets - not access control |
| Logs/events | Operational data, not identity |
| Configurations | Unless they control who can do what |
Entitlement granularity
Fine-grained: Separate entitlements for read, write, admin- Pro: More control in access reviews
- Con: More complexity, more grants to manage
- Pro: Simpler model
- Con: Can’t revoke admin without revoking everything
Parent-child relationships
Some systems have hierarchies: organization -> project -> resource.- Child resources only make sense within a parent context
- You need to scope List() calls to a parent
- The target API is organized hierarchically
Definition of done
Your connector is ready when:- Sync works deterministically (same inputs produce stable IDs and consistent results across runs)
- Pagination works (no token loops; handles large datasets)
- You can run without production ConductorOne credentials (local testing story exists)
Build and test
Common mistakes
Resource type mismatches
A grant references a principal byResourceId (type + id). If your principal type id doesn’t match what you used in ResourceType(), you will create dangling edges.
Implicit capability claims
A connector may have a--provisioning flag but still not implement specific provisioners. Treat “flag exists” as necessary, not sufficient.
API clients
If the service has an official Go SDK, use it. Otherwise, the SDK’suhttp package handles rate limiting and retries:
Error handling
Return errors, don’t panic. Wrap errors with context:Credentials
Never log credentials. Use the SDK’sSecretString type for sensitive config fields:
Next steps
Pagination patterns
Cursor, offset, LDAP paging, and nested pagination with Bag
Provisioning
Grant, revoke, and account lifecycle operations
Deployment
Run modes, credentials, monitoring
Cookbook
Real-world patterns and solutions
Quick reference
Resource traits
| Trait | Use for |
|---|---|
TRAIT_USER | Individual accounts |
TRAIT_GROUP | Collections of users |
TRAIT_ROLE | Permission bundles |
TRAIT_APP | Applications |