Skip to main content
Every target system has its own vocabulary (teams vs groups, roles vs permission sets, projects vs workspaces). This diversity is a strength - each system evolved for its specific purpose. But it creates a challenge: how do you get unified visibility across all of them? Baton solves this by normalizing every system into a consistent shape so ConductorOne can ask one question across all systems: who has access to what? Without this normalization, each system would be an island - auditors would see chaos, and access reviews would require expert knowledge of every platform. The minimal “connector surface area” is expressed through the SDK’s ResourceSyncer interface: list resources, list entitlements, list grants.

The access graph

Your connector produces an access graph that powers access reviews, certification campaigns, provisioning workflows, and compliance reporting. This single data structure drives everything ConductorOne does. The graph has three main node/edge types:
+------------------+     +------------------+     +------------------+
|    RESOURCES     |     |   ENTITLEMENTS   |     |      GRANTS      |
|------------------|     |------------------|     |------------------|
| Things that exist|     | Permissions that |     | Who has what     |
|                  |     | can be assigned  |     |                  |
| - Users          | --> | - Admin access   | --> | Alice has Admin  |
| - Groups         |     | - Read access    |     | on Database X    |
| - Roles          |     | - Member of team |     |                  |
| - Applications   |     |                  |     | Bob is Member    |
+------------------+     +------------------+     | of Team Y        |
                                                  +------------------+
  • Resources: things that exist (users, groups, apps, roles, projects, etc.)
  • Entitlements: permissions you can assign on a resource (member, admin, read, etc.)
  • Grants: facts connecting principals to entitlements (Alice is a member of Engineering)
This is not a theoretical model: these are concrete protobuf types and services in the SDK.

Hierarchical resources

Most access management systems flatten everything into a single list. Baton takes a different approach: resources can have parent-child relationships. This preserves the natural structure of your target systems. Consider GitHub: organizations contain repositories, repositories have branches. Or AWS: accounts contain services, services have resources. When your connector models these hierarchies, ConductorOne can:
  • Show access in context (this role applies to this project, not globally)
  • Enable scoped access reviews (review all access within a single org unit)
  • Support inheritance patterns where they exist in the target system
You express hierarchy through the parentResourceID parameter in your List() method. The SDK calls your List() first with no parent (top-level resources), then again for each parent that might have children. This inversion of control means you describe the structure; the SDK walks it.

Resource types and traits

Every resource has a resource type (string id) and can declare traits that tell ConductorOne how to interpret it. Traits let ConductorOne understand what kind of thing a resource is, even when different systems call it different names.
var userResourceType = &v2.ResourceType{
    Id:          "user",
    DisplayName: "User",
    Traits:      []v2.ResourceType_Trait{v2.ResourceType_TRAIT_USER},
}
The trait enum includes:
  • TRAIT_USER
  • TRAIT_GROUP
  • TRAIT_ROLE
  • TRAIT_APP
  • TRAIT_SECRET
Traits are optional for custom resource types, but they unlock powerful features. When you mark a resource with TRAIT_USER, ConductorOne knows it can correlate that resource with users from other systems, display it in user-centric views, and apply user-specific policies.
TraitUse for
TRAIT_USERIndividual accounts
TRAIT_GROUPCollections of users
TRAIT_ROLEPermission sets
TRAIT_APPApplications or services
TRAIT_SECRETCredentials or tokens

Entitlements

Entitlements define what can be granted. They’re attached to resources - permissions don’t float free in the system, they belong to something specific.
// A group offers membership as an entitlement
entitlement := &v2.Entitlement{
    Id:          "member",
    DisplayName: "Member",
    Resource:    groupResource,
}
One resource can offer multiple entitlements. A GitHub repository might offer: read, write, maintain, admin.

Grants

Grants record who has what:
// Alice is a member of the engineering team
grant := &v2.Grant{
    Principal:   aliceResource,
    Entitlement: memberEntitlement,
}
Grants connect a principal (usually a user) to an entitlement (a permission on a resource).

The sync lifecycle

Understanding the sync lifecycle helps you write cleaner code. The SDK orchestrates everything; you implement the callbacks. The “shape” of work per resource type is:
  • Define type (ResourceType)
  • List instances (List)
  • For each resource instance, list entitlements (Entitlements)
  • For each resource instance, list grants (Grants)
Each method is paginated: you return a list of results and a nextPageToken string (empty when done).
The SDK detects a common pagination bug: returning the same next-page token you were given. This prevents infinite loops from subtle bugs.
Key insight: The SDK processes ALL resource types together for each stage, not one resource type completely before the next. This “inversion of control” pattern keeps your connector code focused on data transformation rather than orchestration logic.
Stage 1: ResourceType()
  - SDK learns what resource types exist (user, group, role, etc.)

Stage 2: List()
  - SDK fetches all instances of each resource type
  - Returns: 127 users, 23 groups, 15 roles

Stage 3: Entitlements()
  - SDK asks each resource what entitlements it offers
  - Returns: group-A offers "member", role-X offers "assigned"

Stage 4: Grants()
  - SDK discovers who has each entitlement
  - Returns: alice has "member" on group-A, bob has "assigned" on role-X

The sync pipeline

When a connector runs, data flows through several stages. The clean separation between what you control and what ConductorOne controls makes the system reliable and testable:
+-----------+    +-----------+    +-------+    +-------+    +--------+
|  External |    | Connector |    |  .c1z |    | Sync  |    | Domain |
|   System  | -> |  (yours)  | -> | File  | -> |Service| -> | Objects|
+-----------+    +-----------+    +-------+    +-------+    +--------+
                                                   |
                                              "Uplift"
  1. Fetch - Your connector calls the external API
  2. Transform - Your connector creates Resource/Entitlement/Grant objects
  3. Output - SDK writes objects to a .c1z file (gzip-compressed SQLite)
  4. Ingest - ConductorOne’s sync service reads the .c1z file
  5. Uplift - Raw connector records become domain objects (Apps, Resources, Grants)
What you control: Steps 1-3. Your connector fetches, transforms, and outputs. What ConductorOne controls: Steps 4-5. The sync service and uplift process.

ID correlation

ConductorOne needs to know whether a resource in this sync is the same resource from a previous sync. This is where the RawId annotation matters. When you build a resource, include its external system ID:
resource, _ := resourceBuilder.NewGroupResource(
    group.Name,
    groupResourceType,
    group.Id,  // Used internally by SDK
    []resource.GroupTraitOption{},
)
// Add the external system's ID for correlation
resource.WithAnnotation(&v2.RawId{Id: group.Id})
The RawId annotation carries the external system’s identifier through the pipeline:
  • Connector: Sets RawId annotation on the resource
  • Sync storage: Stored as external_id on the connector record
  • Domain objects: Tracked in source_connector_ids map
This enables ConductorOne to:
  • Correlate resources across syncs (same ID = same resource)
  • Track which connector discovered which resource
  • Support pre-sync reservation patterns
What value to use: The external system’s native, stable identifier. For Okta, that’s app.Id. For AWS, that’s the ARN. For GCP, that’s the project ID.

ID vocabulary

These terms appear throughout ConductorOne when discussing identity correlation:
TermWhere it appearsPurpose
RawIdConnector output annotationExternal system’s stable identifier, set by connector
external_idSync layer storageSame value stored on ConnectorResource records
source_connector_idsDomain objectsMap of connector_id to external_id for multi-connector scenarios
raw_baton_idDomain objectsSet after merge; the canonical external ID
match_baton_idTerraform/APIPre-sync reservation (allows creating objects before connector discovers them)
The flow is: RawId (connector) -> external_id (sync) -> source_connector_ids or raw_baton_id (domain).

Modeling decisions

Your modeling choices expand or constrain what your organization can do with access control. Two connectors can both be “correct” and still produce very different experiences in ConductorOne:
  • Entitlement granularity
    • Fine-grained (read/write/admin): more precision in reviews and provisioning, more total grants and API calls.
    • Coarse-grained (access/no-access): simpler, but you can’t request/revoke specific privilege levels.
  • Capability surface
    • Sync-only vs sync + provision (Grant/Revoke/Create/Delete)
    • Check per connector/version rather than assuming.
DecisionImpact
Granular entitlements (read, write, admin separately)More control, more complexity
Coarse entitlements (access vs no access)Simpler, less visibility
Group membership as grantsUsers can request to join groups
Roles as resourcesRoles can be granted/revoked
Guidance: Model what matters for access decisions. If you need to revoke admin access separately from read access, make them separate entitlements.

Constraints and guardrails

The SDK includes guardrails that catch common mistakes early:
  • Pagination invariants: your next token must progress; the SDK will detect and error on “same token” loops.
  • Optional advanced behaviors exist but are not universal: targeted sync (Get), account management, resource deletion, credential rotation. Start with the basics; add these when you need them.

Error handling

If an API call fails, return an error. The SDK handles retries for transient failures. For permanent failures (bad credentials, missing permissions), the sync stops with a clear error message.

Rate limiting

Most APIs have rate limits. The SDK’s HTTP client handles backoff automatically, but be mindful of page sizes - smaller pages mean more requests.

Execution modes

Connectors can run in different modes:
ModeTriggerBehavior
One-shotNo --client-idRun, sync to file, exit
Daemon--client-id providedConnect to C1, process tasks continuously
See Deployment for operational details on each mode.

Next steps

Quick reference

Resource types

TypeDescriptionExample
UserIndividual accountsalice@company.com
GroupCollections of usersengineering-team
RolePermission bundlesadmin-role
AppApplicationsbilling-service
TeamGitHub/Slack style teamsplatform-team
ProjectScoped containersproduction-project

The four methods

Every resource syncer implements:
MethodPurposeCalled
ResourceType()Define the resource typeOnce per sync
List()Fetch all instancesMay be called multiple times (pagination)
Entitlements()What permissions does this offer?Once per resource instance
Grants()Who has those permissions?Once per entitlement