Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Provider Accounts

A Provider Account is an account at an AI provider: in v1, a Claude Pro/Max subscription. Each account has a credential and usage limits (windows such as the 5 hour and 7 day windows of a subscription). Admins manage accounts live from the dashboard (Settings > Accounts), the REST API (/api/v1/provider-accounts), the CLI (ironflow accounts) and the MCP server.

When at least one account of the right kind exists, the worker picks one for every agent step, injects its credential into the Claude CLI process, and records the usage windows the CLI reports. With no account, agent steps run with the worker’s own environment, exactly as before.

The claude_subscription kind

Run claude setup-token on a machine logged into the subscription and paste the sk-ant-oat01-... token. Before storing anything, the server checks the format and then sends a one-token request to the Anthropic API:

  • a malformed or rejected token is refused with 422, and nothing is stored;
  • a rate-limited token (429) is stored and shown as limited until its window resets;
  • an unreachable provider gives 502.

Where the credential lives

The credential is stored as the system secret accounts/<id>/credential, encrypted like every other secret. That namespace is hidden from the Secrets page and refused by the Secrets API. No response, log, audit entry or event carries the token.

Injection per transport

TransportHow the token reaches the CLI
Local (ClaudeCodeProvider)CLAUDE_CODE_OAUTH_TOKEN in the child process environment
Docker (DockerProvider)CLAUDE_CODE_OAUTH_TOKEN in the exec environment
SSH (SshProvider)first line of stdin, read by the remote shell and exported
Kubernetesnot yet: agent steps use the pod environment

The token never appears on a command line. The worker forces the CLI into stream-json mode so it can read the rate_limit_event lines that report the windows.

Selection

The worker keeps only available accounts: enabled, token not rejected, not expired, no applicable window rejected until its reset, and under max_concurrency. A window scoped to a model family (for example the Opus 7 day window) only blocks steps using that family. A strategy then picks one:

StrategyPicks
least_utilized (default)the lowest peak utilization, plus 0.15 per running step
prioritythe lowest priority value
round_robinthe next account, by name

Choose it on the worker:

use ironflow_core::account_strategy::Priority;

let worker = WorkerBuilder::new(&api_url, &worker_token)
    .provider(Arc::new(ClaudeCodeProvider::new()))
    .account_strategy(Arc::new(Priority))
    .build()?;

Targeting an account or a pool

By default a step may use any account of the kind. A step can narrow that:

use std::time::Duration;

// Only accounts tagged `team`.
let review = AgentStepConfig::new("Review the diff").account_pool("team");
// Exactly this account: never another one, never the worker's own token.
let audit = AgentStepConfig::new("Audit the release").account("perso-max");

A name that matches no account fails the step with AgentError::AccountNotFound. A pool with no account fails it with AgentError::NoCapacity.

When every account is limited

If the CLI reports a rejected window while a step runs on an account, the worker records it and runs the step again on the next available account. A step pinned with .account(..) has no other account and does not fail over.

When no targeted account is available, the run is not failed: it goes Sleeping until the earliest window reset, and the step runs again from the start when the run wakes. The run shows the provider kind it waits for (capacity_wait_kind in the API, a line in ironflow-cli run get <id>, a note in the dashboard) and its wake-up time in scheduled_at. Adding an account of that kind, re-enabling one or renewing its token wakes the run right away. An account that is only saturated (max_concurrency reached) is tried again after a minute.

The wait is bounded, counting every time the same step was parked: 6 hours by default. A reset further away fails the step with AgentError::NoCapacity, which carries the next reset time. Set the bound per step or on the worker; the step wins:

// Fail fast instead of waiting.
let urgent = AgentStepConfig::new("Triage the incident").fail_fast_on_capacity();

let worker = WorkerBuilder::new(&api_url, &worker_token)
    .provider(Arc::new(ClaudeCodeProvider::new()))
    .max_capacity_wait(Duration::from_secs(2 * 3600))
    .build()?;

A step marked allow_failure() still sleeps: waiting for capacity is not a failure. With no account at all, a rate limit reported on the worker’s own token puts the run to sleep the same way.

CLI

claude setup-token | ironflow accounts add perso-max --token-stdin --tag perso --priority 10
ironflow accounts list
ironflow accounts usage perso-max
ironflow accounts update perso-max --max-concurrency 2
ironflow accounts test perso-max
ironflow accounts remove perso-max --yes

Usage history is kept PROVIDER_ACCOUNT_USAGE_RETENTION_DAYS days (30 by default).