Skip to content

Feature Proposal: Multi-Target Operations #1187

@notgitika

Description

@notgitika

Multi-Target Operations: CLI DevEx Proposal

Author: gitikavj@
Date: 2026-05-08
Status: Draft
Archetype: scope_widening


What is Multi-Target Operations?

Today, aws-targets.json supports an array of named deployment targets (e.g., dev, staging, prod), but the CLI hardcodes resolution to the first entry for most operational commands. Developers who need to work across multiple environments must either manually swap entries or maintain separate project directories — neither is acceptable at scale.

Multi-Target Operations makes the target selection surface first-class across all operational commands. Every command that reads or writes to a deployed environment gains --target <name> and --all-targets flags. The default behavior (no flag) uses the first configured target, preserving full backwards compatibility for single-target projects.

The core gap is narrow and surgical: resolveAgent() in src/cli/operations/resolve-agent.ts hardcodes targetNames[0]. That one callsite, plus consistent flag exposure across logs and traces (the two commands currently missing --target), is the bulk of the work. The multi-target state model in deployed-state.json already supports this — it has been per-target since day one.


Scope

In scope:

  • Adding --target <name> and --all-targets flags to logs and traces (the two commands that currently lack them)
  • Normalizing --target default behavior across deploy, status, and invoke (currently inconsistent)
  • Adding --all-targets flag to deploy, status, invoke, logs, traces
  • Adding --parallel execution flag for --all-targets operations (power-user opt-in)
  • Adding --target and --all-targets to fetch access command
  • Fixing resolveAgent() to accept an explicit target name instead of hardcoding targetNames[0]
  • Schema normalization in aws-targets.json (already supports named targets — no structural change needed)

Out of scope:

  • Console/UI experience
  • Service internals or control plane changes
  • Changes to aws-targets.json format (already correct)
  • Changes to deployed-state.json structure (already per-target)
  • Target creation/management commands (add target, remove target) — covered by direct aws-targets.json editing today

Current State

aws-targets.json is an array of AwsDeploymentTarget objects (defined in src/schema/schemas/aws-targets.ts), each with name, account, region, and optional description. The structure has always supported multiple targets. deployed-state.json (defined in src/schema/schemas/deployed-state.ts) stores state as Record<DeploymentTargetName, TargetDeployedState> — also per-target since inception.

The gap is entirely in the command layer. resolveAgent() (src/cli/operations/resolve-agent.ts) always uses targetNames[0] from deployed state. Of the six operational commands, three have --target flags but with inconsistent defaults, and two (logs, traces) have no --target flag at all.

Key files today:

  • src/cli/operations/resolve-agent.ts — shared agent resolution; hardcodes first target
  • src/cli/commands/deploy/command.tsx — has --target, defaults to literal "default"
  • src/cli/commands/deploy/actions.ts — deploy business logic
  • src/cli/commands/status/command.tsx — has --target, defaults to first deployed target
  • src/cli/commands/status/action.ts — status business logic
  • src/cli/commands/invoke/command.tsx — has --target, defaults to literal "default"
  • src/cli/commands/invoke/action.ts — invoke business logic
  • src/cli/commands/logs/command.tsxno --target flag; uses resolveAgent() → first target
  • src/cli/commands/logs/action.ts — logs business logic
  • src/cli/commands/traces/command.tsxno --target flag; uses resolveAgent() → first target
  • src/cli/commands/traces/action.ts — traces business logic
  • src/schema/schemas/aws-targets.ts — AwsDeploymentTarget Zod schema
  • src/schema/schemas/deployed-state.ts — DeployedState Zod schema (already per-target)
  • src/lib/schemas/io/config-io.ts — resolveAWSDeploymentTargets() with region fallback logic

Current developer experience:

# Single target (works fine):
agentcore deploy                # deploys to "default" target

# Multi-target (broken — user must edit aws-targets.json):
agentcore deploy --target prod  # works on deploy only
agentcore logs                  # always shows first target's logs — no flag to change this
agentcore traces                # always shows first target's traces — no flag to change this

Current --target flag audit:

Command Has --target? Default Notes
deploy Yes "default" (literal) Fails if no target named "default"
status Yes First deployed target Most sensible default
invoke Yes "default" (literal) Falls back to first deployed target
logs No First target via resolveAgent() Gap
traces No First target via resolveAgent() Gap
fetch access No N/A Gap

Target State

Every operational command that touches a deployed environment exposes --target <name> and --all-targets. The default with no flag is the first entry in aws-targets.json (not the literal string "default"). With --all-targets, the command runs against every configured target sequentially, with --parallel available for concurrent execution.

Target developer experience:

# Single target (explicit):
agentcore deploy --target prod
agentcore status --target staging
agentcore invoke --target dev "hello world"
agentcore logs --target prod
agentcore traces --target prod
agentcore fetch access --target prod

# All targets (sequential by default):
agentcore deploy --all-targets
agentcore status --all-targets
agentcore invoke --all-targets "hello world"

# All targets (parallel — power users):
agentcore deploy --all-targets --parallel
agentcore status --all-targets --parallel

# Default (unchanged behavior — uses first target):
agentcore deploy
agentcore status
agentcore invoke "hello world"
agentcore logs
agentcore traces

Coexistence Model

Model: user_chooses_one
Backwards compatible: Yes

Dimension Existing (single-target) New (multi-target)
Triggered by No flag (implicit) --target <name> or --all-targets
Configuration aws-targets.json entry 0 Explicit name or all entries
Deploy mechanism CDK/imperative (unchanged) Same — dispatches per-target
State tracking deployed-state.json targets[first] deployed-state.json targets[name]

Projects with a single entry in aws-targets.json are unaffected. --target defaults to the first configured target. No migration required. No config field changes.

Callout: The default-to-first-entry rule replaces the current default-to-literal-"default" behavior in deploy and invoke. This is a bug fix, not a breaking change — any project whose first target is named "default" (the common case) sees identical behavior.


Developer Journeys

1/ Developer with multiple targets runs deploy to prod

User story: A developer has aws-targets.json with [dev, staging, prod] entries. They want to ship to prod only.

CLI experience:

agentcore deploy --target prod

Under the hood:

  1. handleDeploy() receives options.target = "prod"
  2. resolveAWSDeploymentTargets() reads aws-targets.json — finds 3 entries
  3. Validates "prod" exists in targets — error with list of valid names if not
  4. Deploys CDK stack for prod target only
  5. Writes deployed-state.json at targets.prod

What we build:

  • No new code for deploy — --target already exists; behavior is correct

2/ Developer deploys to all targets in sequence

User story: Same developer wants to promote a release to all three environments in one command.

CLI experience:

agentcore deploy --all-targets

# Output:
# [1/3] Deploying to dev...   ✓ (12s)
# [2/3] Deploying to staging... ✓ (14s)
# [3/3] Deploying to prod...  ✓ (11s)

Under the hood:

  1. handleDeploy() receives options.allTargets = true
  2. Reads all entries from aws-targets.json
  3. Iterates sequentially (or in parallel if --parallel)
  4. Calls existing single-target deploy logic per entry
  5. Aggregates results; prints per-target status
  6. Exits non-zero if any target fails; reports which targets failed

What we build:

  • --all-targets flag on deploy, status, invoke, logs, traces, fetch access
  • --parallel flag (companion to --all-targets)
  • runForAllTargets() utility in src/cli/operations/multi-target.ts
  • Per-target result aggregation and exit code logic

3/ Developer checks logs for a specific target

User story: Developer wants to tail logs for the prod deployment of their agent.

CLI experience:

agentcore logs --target prod
agentcore logs --target prod --runtime my-agent

Under the hood:

  1. logs command receives options.targetName = "prod"
  2. Passes targetName to resolveAgent() instead of relying on targetNames[0]
  3. resolveAgent() uses the explicit targetName to look up state in deployed-state.json
  4. CloudWatch logs fetched for the resolved runtime in the prod region

What we build:

  • --target <name> flag on logs command (src/cli/commands/logs/command.tsx)
  • --target <name> flag on traces command (src/cli/commands/traces/command.tsx)
  • resolveAgent() accepts optional targetName param; uses it when provided

4/ Existing single-target project — unchanged (proves backwards compat)

User story: Developer has one entry in aws-targets.json named "default". They never use --target.

CLI experience:

# Everything works exactly as before:
agentcore deploy    # uses first (and only) target: "default"
agentcore status    # uses first deployed target: "default"
agentcore invoke "hello"  # uses first deployed target: "default"
agentcore logs      # uses first deployed target: "default"
agentcore traces    # uses first deployed target: "default"

Under the hood:

  1. No --target or --all-targets flag provided
  2. Code reads aws-targets.json[0] as the target (same as today for status; fixes deploy/invoke default bug)
  3. All existing code paths invoked identically

Callout: This is the regression test. If this journey breaks after the change ships, it's a blocker.


Affected Commands

Command Change Type Description Backwards Compatible
agentcore deploy new_flag Add --all-targets and --parallel; normalize default to first target (not "default") Yes
agentcore status new_flag Add --all-targets and --parallel Yes
agentcore invoke new_flag Add --all-targets and --parallel; normalize default to first target (not "default") Yes
agentcore logs new_flag Add --target <name>, --all-targets, --parallel Yes
agentcore traces new_flag Add --target <name>, --all-targets, --parallel Yes
agentcore fetch access new_flag Add --target <name>, --all-targets Yes

Schema Changes

No changes to agentcore.json or deployed-state.json schema. Both already support multiple targets. No new fields.

Callout: The only "schema" change is behavioral: the unset---target default moves from the literal string "default" to awsTargets[0].name. This is enforced in command option parsing, not schema validation.

In aws-targets.json (no structural change):

[
  { "name": "dev",     "account": "111122223333", "region": "us-west-2" },
  { "name": "staging", "account": "444455556666", "region": "us-west-2" },
  { "name": "prod",    "account": "777788889999", "region": "us-east-1" }
]

In deployed-state.json (no structural change):

{
  "targets": {
    "dev":     { "resources": { "runtimes": { "my-agent": { "runtimeId": "..." } } } },
    "staging": { "resources": { "runtimes": { "my-agent": { "runtimeId": "..." } } } },
    "prod":    { "resources": { "runtimes": { "my-agent": { "runtimeId": "..." } } } }
  }
}

Detection & Prerequisites

No external tool dependencies. No detection logic needed.

Target validation error messages:

Scenario Message
--target name not in aws-targets.json Target "prod" not found. Available targets: dev, staging (from aws-targets.json)
--all-targets with empty aws-targets.json No targets configured. Add entries to aws-targets.json first.
--target + --all-targets together --target and --all-targets are mutually exclusive.
--parallel without --all-targets --parallel requires --all-targets.

Codebase Changes

New Files

File Purpose
src/cli/operations/multi-target.ts runForAllTargets() utility — sequential/parallel loop with result aggregation

Modified Files

File Change
src/cli/operations/resolve-agent.ts Accept optional targetName?: string param; use it instead of targetNames[0] when provided
src/cli/commands/deploy/command.tsx Normalize default: awsTargets[0].name instead of "default"; add --all-targets, --parallel
src/cli/commands/deploy/actions.ts Thread target through multi-target loop when --all-targets
src/cli/commands/status/command.tsx Add --all-targets, --parallel
src/cli/commands/status/action.ts Thread target through multi-target loop when --all-targets
src/cli/commands/invoke/command.tsx Normalize default; add --all-targets, --parallel
src/cli/commands/invoke/action.ts Thread target through multi-target loop; pass targetName to resolveAgent()
src/cli/commands/logs/command.tsx Add --target <name>, --all-targets, --parallel
src/cli/commands/logs/action.ts Pass targetName to resolveAgent()
src/cli/commands/traces/command.tsx Add --target <name>, --all-targets, --parallel
src/cli/commands/traces/action.ts Pass targetName to resolveAgent()
src/cli/commands/fetch-access/command.tsx Add --target <name>, --all-targets
src/cli/commands/fetch-access/action.ts Thread target name through resolution

Architecture: Where It Branches

Dispatch Point

agentcore [command] [args]
  │
  ├─ --target <name>      → resolves single named target → existing single-target path
  ├─ --all-targets        → runForAllTargets() → iterates aws-targets.json entries
  │                          └─ [--parallel] → Promise.all vs sequential loop
  └─ (no flag)            → awsTargets[0] → existing single-target path (unchanged behavior)

Pattern: Dispatch at command option parsing — no Strategy class needed. runForAllTargets() is a thin iterator that calls the existing single-target action function per entry.

Precedent in the codebase: src/cli/commands/status/action.ts already implements the dispatch pattern — it reads deployedTargetNames, selects options.targetName ?? targetNames[0], and passes the name down. The deploy and invoke normalization fix copies this exact pattern.

Deploy Flow Change

No change to the deploy flow phases. Multi-target with --all-targets is the existing flow executed N times in sequence (or parallel). The CDK path, preflight, state write — all unchanged per invocation.

Current:                                  New (--all-targets):
  1. Preflight validation                   FOR EACH target in aws-targets.json:
  2. Identity providers                       1. Preflight validation
  3. OAuth2 providers                         2. Identity providers
  4. CDK synth + deploy                       3. OAuth2 providers
  5. Parse outputs → deployed-state.json      4. CDK synth + deploy
  6. Post-deploy operations                   5. Parse outputs → deployed-state.json
                                              6. Post-deploy operations
                                            Report: 3/3 succeeded | 1/3 failed (prod)

Architectural Decisions

# Decision Choice Rationale
1 Where to fix resolveAgent() Add optional targetName param Backwards compatible — existing callers pass nothing, get current behavior
2 Default target resolution awsTargets[0].name (not "default") Matches status command's already-correct behavior; fixes latent bug in deploy/invoke
3 --all-targets execution model Sequential default, --parallel opt-in Sequential is safer for deploy (CDK limits, IAM propagation); parallel useful for status/logs
4 Multi-target utility location src/cli/operations/multi-target.ts Consistent with existing operations directory; keeps command files thin
5 --target + --all-targets mutual exclusivity Error at flag parse time Unambiguous intent; avoids silent override of one by the other
6 Schema changes None deployed-state.json and aws-targets.json already model multi-target correctly

Implementation Phases

Phase 1: Fix resolveAgent() and normalize defaults (independently shippable)

  • Add targetName?: string param to resolveAgent() in src/cli/operations/resolve-agent.ts
  • When targetName provided: use it to look up state; error clearly if not found
  • When targetName absent: preserve current behavior (targetNames[0]) — no behavior change
  • Normalize deploy and invoke default: replace "default" literal with awsTargets[0].name
  • Unit tests: resolveAgent with explicit target, resolveAgent with absent target, resolveAgent default
  • Ship note: After this lands, zero behavior change for single-target projects. Bug fix for projects whose first target isn't named "default".

Phase 2: Add --target to logs, traces, fetch access (independently shippable)

  • Add --target <name> to logs command (command.tsx + pass to action.ts)
  • Add --target <name> to traces command (command.tsx + pass to action.ts)
  • Add --target <name> to fetch access command (command.tsx + pass to action.ts)
  • Thread targetName through to resolveAgent() in all three action files
  • Unit tests: logs with explicit target, traces with explicit target, invalid target error
  • Ship note: After this lands, logs, traces, and fetch access behave consistently with deploy, status, invoke.

Phase 3: Add --all-targets and --parallel (independently shippable)

  • Write runForAllTargets() in src/cli/operations/multi-target.ts
    • Reads resolveAWSDeploymentTargets() to get ordered target list
    • Accepts action function, options, and parallel: boolean
    • Sequential: for-await loop; Parallel: Promise.allSettled
    • Returns per-target results with success/failure and output
  • Add --all-targets and --parallel flags to all six commands
  • Implement mutual exclusivity validation (--target + --all-targets → error)
  • Implement --parallel without --all-targets → error
  • Per-target progress output: [1/3] Deploying to dev... ✓ (12s)
  • Aggregate exit code: non-zero if any target fails
  • Unit tests: sequential loop, parallel loop, partial failure, mutual exclusivity errors
  • Ship note: After this lands, all multi-target workflows are available.

Phase 4: Polish (independently shippable)

  • Update help text for all six commands to document new flags
  • Update docs/commands.md and docs/configuration.md
  • Add E2E test: deploy to two targets in sequence; verify both appear in deployed-state.json
  • Edge cases: --all-targets with one target (degenerate case — should work identically to --target first)
  • Ship note: Documentation and E2E coverage for the complete feature.

Testing Strategy

Existing Test Preservation

All existing tests must pass without modification after Phases 1-3. The changes to resolveAgent() are purely additive (optional param). The normalize-default fix only breaks projects with a misconfigured first target named something other than "default" — which is the target of the fix. Run npm test, npm run typecheck, npm run lint on every PR.

Precedent in the codebase: src/cli/commands/status/action.ts implements target selection with options.targetName ?? targetNames[0]. Existing status tests already cover the no-flag path. We verify that pattern still passes after we extend it to the other commands.

New Test Matrix

Scenario No flag (default) --target prod --all-targets --all-targets --parallel
deploy ✓ (first target)
status ✓ (first target)
invoke ✓ (first target)
logs ✓ (first target)
traces ✓ (first target)
fetch access ✓ (first target) N/A N/A
Invalid --target name N/A Error + list N/A N/A
--target + --all-targets N/A Error N/A N/A
--parallel without --all-targets N/A Error N/A N/A
Partial failure (one target fails) N/A N/A Non-zero exit Non-zero exit

Integration / E2E

  • Against account 998846730471 in us-west-2 and us-east-1:
  • Deploy two-target project; verify deployed-state.json has both target entries
  • agentcore status --all-targets shows both target states
  • agentcore logs --target <name> streams correct CloudWatch log group
  • agentcore deploy --target <name> does not touch the other target's stack

Open Questions

# Question For Context
1 Should --all-targets on invoke be supported? It's operationally unusual to invoke an agent on every target at once. gitikavj@ / PM Included in scope per inputs, but worth a deliberate decision. Power-user edge case only.
2 For --all-targets --parallel on deploy: should there be a max-concurrency cap, or unlimited? gitikavj@ Unlimited could hit CDK rate limits or IAM propagation races if many targets exist.
3 Should failed targets in --all-targets be retried, or fail-fast? gitikavj@ / Eng Current proposal: no retry, non-zero exit with which targets failed.
4 Does fetch access support --all-targets? The inputs include it, but access tokens are per-target by definition. gitikavj@ Included per inputs. Need confirmation that listing all-target tokens is a real use case.

Escalation Required

  • Default target string "default" vs first-entry: Changing the default from the literal string "default" to awsTargets[0].name is my recommendation, but it technically changes behavior for projects where awsTargets[0].name !== "default". This is a latent bug fix, but the AgentCore CLI team should sign off before Phase 1 ships.
  • --parallel on deploy: Parallel CDK deployments to multiple targets in the same account/region may hit CloudFormation stack operation limits. Need confirmation from the CDK/infra team on whether throttling is a risk here, and whether a max-concurrency cap is needed.

Appendix

Side-by-Side: Single-Target vs Multi-Target Project

Single-target project (unchanged):

aws-targets.json:

[
  { "name": "default", "account": "998846730471", "region": "us-west-2" }
]
agentcore deploy          # same as today
agentcore status          # same as today
agentcore logs            # same as today — resolves to "default"

Multi-target project (new capability):

aws-targets.json:

[
  { "name": "dev",     "account": "111122223333", "region": "us-west-2" },
  { "name": "staging", "account": "444455556666", "region": "us-west-2" },
  { "name": "prod",    "account": "777788889999", "region": "us-east-1" }
]
agentcore deploy                     # deploys to dev (first entry)
agentcore deploy --target prod       # deploys to prod only
agentcore deploy --all-targets       # deploys dev → staging → prod in sequence
agentcore status --all-targets       # shows status for all three
agentcore logs --target prod         # streams prod logs

resolveAgent() Change (Sketch)

Before:

// src/cli/operations/resolve-agent.ts
const targetName = targetNames[0]; // always first

After:

// src/cli/operations/resolve-agent.ts
function resolveAgent(
  context: DeployedProjectConfig,
  options: { runtime?: string; targetName?: string }  // targetName added
) {
  const targetName = options.targetName ?? targetNames[0]; // explicit wins
  // ... rest unchanged
}

runForAllTargets() Sketch

// src/cli/operations/multi-target.ts
export async function runForAllTargets<T>(
  targets: AwsDeploymentTarget[],
  action: (target: AwsDeploymentTarget) => Promise<T>,
  options: { parallel?: boolean }
): Promise<Array<{ target: string; result: T | Error }>> {
  if (options.parallel) {
    const results = await Promise.allSettled(targets.map(action));
    return targets.map((t, i) => ({
      target: t.name,
      result: results[i].status === 'fulfilled' ? results[i].value : results[i].reason,
    }));
  }
  const results = [];
  for (const target of targets) {
    try {
      results.push({ target: target.name, result: await action(target) });
    } catch (err) {
      results.push({ target: target.name, result: err as Error });
    }
  }
  return results;
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions