Gopher says
The connector is built. The mock server is running. Now it’s time to actually run a sync, look at what came out, and make sure every resource, entitlement, and grant is exactly what you expect.
Once validation passes, you’ll connect to your C1 tenant and let the connector run for real.
One-Shot Mode
In Level 14 you learned that omitting --client-id and --client-secret runs the connector in one-shot mode. The connector calls the target app once, writes the results to a local file, and exits. This is the primary development workflow.
Point baton-junction at your mock server from Level 18:
./baton-junction \
--app-client-id "<your-app-client-id>" \
--app-client-secret "<your-app-client-secret>" \
--base-url "http://localhost:8080"
When the sync completes, the connector writes sync.c1z in the current directory. This is a compressed archive of every resource, entitlement, and grant the connector discovered. The -f flag lets you write to a different path:
./baton-junction \
--app-client-id "<your-app-client-id>" \
--app-client-secret "<your-app-client-secret>" \
--base-url "http://localhost:8080"
-f my-test-sync.c1z
The Baton CLI Tool
The baton CLI tool reads sync.c1z files and lets you inspect what the connector produced. It’s a separate binary from your connector - install it with:
brew install conductorone/baton/baton
Like your connector, baton --help shows all available subcommands.
Inspecting Resources
Start by checking that the connector discovered all expected resource types and instances:
baton resources -f sync.c1z
Sample output:
ID | Display Name | Resource Type | Parent Resource
r1 | Single Ride | Role | -
r2 | Standard Day | Role | -
r3 | Dining Access| Role | -
r4 | First Class | Role | -
r5 | Express | Role | -
g1 | Regional Pass| Group | -
g2 | Express Pass | Group | -
g3 | All-Access Pass | Group | -
u1 | Alice Smith | User | -
u2 | Bob Jones | User | -
u3 | Carol White | User | -
u4 | Dave Brown | User | -
u5 | Eve Davis | User | -
u6 | Frank Miller | User | -
What to look for:
- All resource types present - you should see User, Group, and Role (or whatever your connector syncs)
- Display names correct - names should be human-readable, not raw IDs
- No duplicates - each resource appears exactly once
- Counts match - the mock server has 6 users, 3 groups, and 5 roles, so the output should match
If a resource type is missing entirely, check that the builder is registered in ResourceSyncers(). If display names are wrong, check the resource.NewUserResource / resource.NewGroupResource calls in your builders.
Inspecting Entitlements
Every group and role should expose at least one entitlement (typically member):
baton entitlements -f sync.c1z
Sample output:
ID | Display Name | Resource Type | Resource | Permission
group:g1:member | Regional Pass Member | Group | Regional Pass | member
group:g2:member | Express Pass Member | Group | Express Pass | member
group:g3:member | All-Access Pass Member | Group | All-Access Pass| member
role:r1:member | Single Ride Member | Role | Single Ride | member
role:r2:member | Standard Day Member | Role | Standard Day | member
role:r3:member | Dining Access Member | Role | Dining Access | member
role:r4:member | First Class Member | Role | First Class | member
role:r5:member | Express Member | Role | Express | member
What to look for:
- Every group and role has a
memberentitlement - if one is missing, theEntitlements()method on that builder isn’t returning it - Entitlement IDs follow the pattern
{resourceType}:{resourceId}:{slug}- the SDK generates these automatically fromentitlement.NewAssignmentEntitlement - Display names are present - these surface in the C1 UI and help operators understand what each entitlement means
Inspecting Grants
Grants are the core of the access data - they link principals (users) to entitlements (group/role memberships):
baton grants -f sync.c1z
Sample output:
ID | Resource Type | Resource | Entitlement | Principal
group:g1:member:user:u2 | Group | Regional Pass | Regional Pass Member | Bob Jones
group:g2:member:user:u3 | Group | Express Pass | Express Pass Member | Carol White
group:g3:member:user:u1 | Group | All-Access Pass | All-Access Pass Member | Alice Smith
role:r1:member:user:u6 | Role | Single Ride | Single Ride Member | Frank Miller
role:r2:member:user:u4 | Role | Standard Day | Standard Day Member | Dave Brown
role:r2:member:group:g1 | Role | Standard Day | Standard Day Member | Regional Pass
role:r2:member:user:u2 | Role | Standard Day | Standard Day Member | Bob Jones
role:r3:member:user:u2 | Role | Dining Access | Dining Access Member | Bob Jones
role:r3:member:user:u3 | Role | Dining Access | Dining Access Member | Carol White
role:r3:member:user:u4 | Role | Dining Access | Dining Access Member | Dave Brown
role:r3:member:role:r4 | Role | Dining Access | Dining Access Member | First Class
role:r3:member:user:u5 | Role | Dining Access | Dining Access Member | Eve Davis
role:r3:member:group:g3 | Role | Dining Access | Dining Access Member | All-Access Pass
role:r3:member:user:u1 | Role | Dining Access | Dining Access Member | Alice Smith
role:r4:member:user:u5 | Role | First Class | First Class Member | Eve Davis
role:r4:member:group:g3 | Role | First Class | First Class Member | All-Access Pass
role:r4:member:user:u1 | Role | First Class | First Class Member | Alice Smith
role:r5:member:group:g2 | Role | Express | Express Member | Express Pass
role:r5:member:user:u3 | Role | Express | Express Member | Carol White
The output includes both connector-returned grants and expanded grants. The group-principal grants (e.g., role:r2:member:group:g1) and role-principal grants (e.g., role:r3:member:role:r4) are the expandable grants from Level 15. The platform expanded them into inherited grants: Bob gets Standard Day via Regional Pass, Carol gets Express via Express Pass, Eve gets Dining Access via First Class, and Alice gets both First Class and Dining Access through the deep chain All-Access Pass -> First Class -> Dining Access.
What to look for:
- Grants link the right principals to the right entitlements - cross-reference with your mock server’s seed data
- Direct, expandable, and expanded grants all appear - direct grants have
user:principals, expandable grants havegroup:orrole:principals, expanded grants are the inherited copies - Grant IDs encode the full relationship -
{resourceType}:{resourceId}:{slug}:{principalType}:{principalId}lets you trace exactly who has what - No orphaned grants - every resource and entitlement in a grant should also appear in the resources and entitlements output
List all grants from a sync file at ./test-output.c1z:
Other Useful Commands
The baton CLI tool has several more subcommands for deeper inspection:
| Command | What It Does |
|---|---|
baton stats -f sync.c1z | Quick summary: counts of resources, entitlements, and grants by type |
baton resource-types -f sync.c1z | Lists all resource types the connector registered |
baton access -f sync.c1z --resource-type user --resource u2 | Shows effective access for a specific user across all entitlements |
baton diff -f sync.c1z | Compares the latest two syncs within the file and shows what changed |
baton stats is especially useful as a quick sanity check:
Type | Count
entitlements | 8
grants | 19
group | 3
resource_types | 3
role | 5
user | 6
If the counts are wildly off from what you expect, something is wrong in your builders before you need to dig into individual records.
A single sync.c1z file can contain multiple sync snapshots. Each time you
run the connector with the same -f path, a new snapshot is appended. baton diff -f sync.c1z compares the latest two snapshots and shows what changed -
added or removed resources, entitlements, and grants. This catches regressions
early: if you refactored the group builder and suddenly lost half the grants,
the diff makes it obvious.
Log Levels
During development and validation, it’s helpful to always run with --log-level debug --log-format console.
./baton-junction \
--client-id "<your-tenant-client-id>" \
--client-secret "<your-tenant-client-secret>" \
--base-url "http://localhost:8080" \
--log-level debug \
--log-format console
--log-format console switches from JSON to human-readable output. --log-level debug surfaces every API call, every resource built, and every grant created. The zap structured logging you added throughout the connector (the l.Debug, l.Info, l.Error calls) all appear here - this is exactly why you added them.
What to watch for:
- API errors - HTTP status codes in the debug logs show if the target app is rejecting requests
- Pagination issues - if the connector stops fetching after the first page, the page token handling in your
Listmethod may be wrong - Missing fields - if a resource has no display name, trace back through the debug logs to see what the API returned
- Grant expansion - debug logs show when group-principal and role-principal grants are created with
GrantExpandableannotations
Reserve --log-level info and --log-format json for service mode, where structured JSON lines integrate with log aggregation systems. For one-shot development, debug + console is always the right choice.
Going to Service Mode
After validation passes, you’re ready to connect to your C1 tenant.
- Create the connector in the C1 admin console and select the connector type
- Generate credentials - the tenant gives you a
client-idandclient-secret - Run with credentials:
./baton-junction \
--client-id "<your-tenant-client-id>" \
--client-secret "<your-tenant-client-secret>" \
--app-client-id "<your-app-client-id>" \
--app-client-secret "<your-app-client-secret>" \
--base-url "http://localhost:8080"
With --client-id and --client-secret present, the connector switches to service mode. Instead of running once and exiting, it:
- Connects to the C1 tenant and registers itself
- Polls the tenant for tasks (syncs, provisioning requests, action invocations)
- Executes tasks and reports results back to the tenant
- After each sync, waits for the interval the tenant specifies (default one hour) before polling again
The tenant never calls your connector. The connector always initiates outbound connections to the tenant. This means the connector can run behind a firewall or NAT without any inbound port configuration.
Gopher says
From go mod init to a running connector connected to C1 - you’ve built the
whole thing. Every resource synced, every grant mapped, every action wired,
and now validated and running live.
The patterns you learned here - resource builders, provisioning,
structured logging, the baton CLI tool - apply to every Baton connector. Pick
any target app with an API, and you know how to build a connector for it.

