If you're running agents on your platform today, there's a decent chance the underlying identity model is service accounts — credentials owned by the org, not by any specific human, with a shared audit trail. It's the cheapest decision and the most expensive one. The cheap part: setup. The expensive part: the moment you have to investigate "what did the agent do" and the answer terminates at the org instead of at a specific human.
This piece is the actual migration. Not the case for why (the pillar essay makes that case). The sequence of schema changes, the credential reissue, the audit log rebinding, the rollout plan. We did this migration at Dock in late 2025 and it took a small team about two weeks of focused work. The shape is portable.
What we mean by "service account" vs "agent identity"
Quick framing so the rest of the piece is clear:
Service account: a credential not tied to any specific human. Created by an admin. Used by automation. Lives independently of any user's lifecycle. If the admin leaves, the service account stays.
Agent identity: a credential tied to a specific owning human. Created by the owner (often self-service). Used by the owner's agents. Lifecycle linked to the owner. If the owner leaves, the agent is offboarded with them.
The contrast is in the owner link, not in the credential mechanism. Both might use API keys, OAuth tokens, or signed JWTs. The difference is whether there's a foreign key to a specific human in the data model. That foreign key is what makes audit attribution work and access propagation tractable.
For the full architectural argument, see service accounts vs agent identities.
Diagnostic: do you actually need to migrate?
Before you migrate, confirm that you need to. Three questions:
- Are agents in your platform doing actions you'd want to attribute to a specific human? If your agents only do truly org-owned automation (nightly cleanup jobs, billing reconciliation, system housekeeping), the service-account model is correct and migration is unnecessary churn.
- Has the audit log become hard to query when something goes wrong? When a row was updated incorrectly, can you tell which human's agent did it? If the audit log says "service-account-7" and you have to ask in Slack which human launched it, you have the diagnostic.
- Are you about to scale past 10-15 agents per org? Below 10, the service-account pattern works because the team can hold the mapping in their heads. Past 10-15, the indirection collapses and the migration starts paying for itself.
If you answered yes to at least two of the three, migrate. If you answered yes to only one, instrument first (start logging who launched each agent) and revisit in a quarter.
How to migrate from service accounts to agent identities
Six steps. Run them in order. Each one is reversible until you delete the old credentials in step 6.
-
Add the
Agenttable withownerUserIdrequired. Schema first. The newAgentrow carriesid,name,ownerUserId(FK, required),credentialId(link to the credential store),createdAt,revokedAt. Don't drop the existing service-account table yet — we'll backfill against it. Confirm the migration in staging: every NEW agent goes through the new table, the old service accounts continue to work. -
Inventory the existing service accounts and assign owners. For each service account currently in use, find the human who is operationally responsible for it. This is the conversation that exposes how the org thinks about ownership. Some service accounts will be cleanly owner-attributable; some will land in the "shared by the platform team" bucket. For the shared bucket, designate one human as the owner of record (usually the team lead). Store the mapping in a temporary
service_account_owner_maptable. -
Issue per-agent credentials for the new identities. For every (service_account, owner) pair, mint a new credential bound to the new
Agentrow. Don't reuse the old credential — the whole point is to have a separate credential per agent so revocation, rotation, and audit work cleanly. Distribute the new credentials to the owners; they replace the old credentials in their tooling. -
Dual-write the audit log during the cutover window. For ~1 week (or one full audit cycle, whichever is longer), have your action handlers record BOTH the old service-account id AND the new agent id on every row. This lets you investigate incidents during the cutover using either identity. After the cutover window closes, the dual-write goes away; you write only the new agent id.
-
Rebind ongoing usage. Each agent's operator switches their tooling from the old service-account credential to the new agent credential. The old credential stays alive but its usage trends to zero. Monitor
service_account_idusage in your event logs; when usage on a given service account hits zero for ~3 days, you know that agent's migration is done. -
Revoke the old credentials. Once every service account has had zero usage for 3+ days, revoke its credential server-side and mark the corresponding row deprecated. Wait one more week (the "did we forget anyone" buffer), then drop the
service_account_owner_mapand the deprecated rows. The migration is done.
The whole sequence takes about 2 weeks if you're disciplined about steps 4 and 5. Most of the time is the soak window in step 5; the actual code changes are 2-3 days.
What changes in the audit log
The biggest operational difference: before the migration, your audit log is keyed by actor_id where actor is a service account. After the migration, it's dual-keyed: actor_id is the agent, accountable_principal_id is the owning human.
Sample SQL queries that work differently:
Before (service-account world):
-- "what did the platform do this week"
SELECT * FROM audit_log
WHERE actor_type = 'service_account'
AND created_at > now() - interval '7 days';
-- "who's responsible for service-account-7's actions?"
-- (no answer; the data model doesn't capture it)
After (agent-identity world):
-- "what did Govind's agents do this week"
SELECT * FROM audit_log al
JOIN agent a ON al.actor_id = a.id
WHERE a.owner_user_id = $1 -- Govind's user id
AND al.created_at > now() - interval '7 days';
-- "who's responsible for agent-7's actions?"
SELECT u.email FROM "user" u
JOIN agent a ON a.owner_user_id = u.id
WHERE a.id = $1;
The second query is the one that exists only after the migration. It's what makes accountability tractable. Six months from now you can answer "which human is on the hook for this action" with one join, not a Slack archaeology session.
Common gotchas
A few patterns we hit at Dock and that we've seen at other teams:
The "shared automation" service account. Some service accounts genuinely don't have a single owner — they're owned by the team. Pick one human as the owner of record (usually the team lead), document the convention, and revisit if the team grows past the lead's bandwidth.
Long-lived credentials in CI / cron systems. If a service account's credential is hardcoded into a CI pipeline or a cron job, the migration touches those systems too. Plan for it: the day you rotate the service account's credential, the cron job needs the new agent credential or it'll fail. Inventory these BEFORE step 3.
External integrations that rely on the old credential. Some service accounts are credentials passed to vendor systems (Slack, Stripe, etc). Those vendor systems don't care about your new agent model — they want a working credential. The migration there is mechanical: rotate the vendor credential in lockstep with rotating the internal credential.
Backfilling the audit log to the new model. Tempting; usually not worth it. The audit log captures what happened in the old model; rebinding old events to new identities is mostly fiction. Leave the old events as they are; new events use the new model. Two query patterns (pre- and post-migration) is the right cost.
Forgetting to delete the dual-write code. After step 6, the dual-write from step 4 needs to come out. It's easy to leave it because "it doesn't hurt." It does hurt: it doubles your audit log write volume forever. Schedule the removal as a follow-up PR before you celebrate the migration as done.
FAQ
How long does this migration take?
About 2 weeks if you're a small team doing it as focused work. The actual code changes are 2-3 days; most of the time is the soak window in step 5 and the buffer period in step 6.
Can I migrate one agent at a time, or do I need to do all at once?
One at a time, by design. Steps 4-5 work per-agent. You can pilot on a single low-risk agent, observe for a few days, then continue with the rest. The migration doesn't require a flag day.
What if some agents genuinely don't have a human owner?
Those are the org-owned automations (nightly cleanup, billing batches) where the service-account pattern is actually correct. Don't migrate them. Keep two patterns in your data model: Agent (owner-linked) for human-attached automation, and ServiceAccount (org-linked) for org-owned automation. The two coexist cleanly; just be clear about which is which.
What happens to existing audit log entries?
They stay attributed to the service account that created them. Don't backfill — see the gotcha above. Your audit log just has a pre-migration era (queries use the old pattern) and a post-migration era (queries use the new pattern). Document the cutover date.
Does this work if I'm not on Postgres?
Yes. The pattern is shape-only: add an Agent table with required ownerUserId, dual-write during cutover, switch usage, retire the old credentials. The specific SQL is illustrative. Same shape works on MySQL, on DynamoDB with composite keys, on any storage layer that supports relational integrity.
Do I need to change anything in the agent's runtime code?
Mostly the credential the runtime presents (step 5). The action handlers that write audit logs need to record actor_id = agent.id, accountable_principal_id = agent.owner_user_id instead of actor_id = service_account.id. About 10-20 lines per handler if your audit-log layer is centralized.
What if my service accounts have permissions narrower than the owners would have?
Then the migration tightens scope: the new agent inherits from the owner with explicit narrowing. Or you keep the per-agent scope override pattern. Either works; pick the one that fits your access model. See signed-agent inheritance for the patterns.
Will the migration break my external integrations?
Only the ones that hardcode the old credential. Inventory those before step 3 and rotate them in lockstep with the internal credential changes.
Where Dock fits
If you're running this migration on Dock, most of it is mechanical because the data model already has the Agent table with ownerUserId and the audit log is already dual-keyed. The work is on YOUR side: assigning owners, rotating credentials, retiring the old patterns. Dock's audit log and access model don't need to change.
If you're running it on your own platform, the pattern above is portable. We've seen 3 other teams run a substantially similar migration in 2026; each took ~2 weeks and none had to roll back.
Read next
- AI agent identity: the design model nobody has standardized — the pillar that argues why you'd want this in the first place.
- Service accounts vs agent identities — the architectural contrast in detail.
- Signed-agent inheritance — the access-propagation pattern that pairs with the migration.
- Agents borrowing human credentials — the anti-pattern that's adjacent (and that the migration also fixes).
- Best AI agent identity providers in 2026 — the buyer guide if you're picking a platform to migrate TO.
