Skip to main content
Maker mode transforms natural language descriptions into executable cloud infrastructure plans. It follows a plan-then-apply workflow inspired by Terraform, but uses native cloud provider CLIs (AWS, GCP, Azure, Cloudflare).

How it works

Maker mode operates in two phases:
1

Plan: Generate CLI commands

An LLM converts your natural language request into a JSON plan containing sequential CLI commands.
clanker ask --maker "Deploy a Lambda function for processing images"
2

Apply: Execute the plan

After review, execute the plan to create resources:
clanker ask --apply < plan.json

Plan structure

A plan is a JSON document with this schema:
{
  "version": 1,
  "createdAt": "2026-03-01T10:30:00Z",
  "provider": "aws",
  "question": "Create an S3 bucket for logs",
  "summary": "Creates an S3 bucket with server-side encryption",
  "commands": [
    {
      "args": ["s3api", "create-bucket", "--bucket", "my-logs-<DEPLOY_ID>", "--region", "us-east-1"],
      "reason": "Create the S3 bucket",
      "produces": {"BUCKET_NAME": "$.Location"}
    },
    {
      "args": ["s3api", "put-bucket-encryption", "--bucket", "<BUCKET_NAME>", "--server-side-encryption-configuration", "{...}"],
      "reason": "Enable encryption at rest"
    }
  ],
  "notes": ["Bucket name includes unique deploy ID to avoid conflicts"]
}

Key fields

  • version: Plan format version (currently 1)
  • provider: Cloud provider (aws, gcp, azure, cloudflare)
  • commands: Ordered array of CLI commands to execute
  • produces: Maps output values to placeholders (e.g., resource IDs)
  • notes: Human-readable explanations

Placeholders

Placeholders are variables that get resolved during execution:

Static placeholders

  • <DEPLOY_ID>: Unique identifier for this deployment
  • <REGION>: AWS region
  • <ACCOUNT_ID>: AWS account ID
  • <APP_PORT>: Application port (inferred from target group)

Dynamic placeholders

Commands can produce values for later use:
{
  "args": ["ec2", "create-vpc", "--cidr-block", "10.0.0.0/16"],
  "produces": {"VPC_ID": "$.Vpc.VpcId"}
}
Now <VPC_ID> can be used in subsequent commands:
{
  "args": ["ec2", "create-subnet", "--vpc-id", "<VPC_ID>", "--cidr-block", "10.0.1.0/24"]
}
func applyPlanBindings(args []string, bindings map[string]string) []string {
    if len(args) == 0 || len(bindings) == 0 {
        return args
    }
    out := make([]string, 0, len(args))
    for _, a := range args {
        if !strings.Contains(a, "<") || !strings.Contains(a, ">") {
            out = append(out, a)
            continue
        }
        rewritten := planPlaceholderTokenRe.ReplaceAllStringFunc(a, func(m string) string {
            key := strings.TrimSuffix(strings.TrimPrefix(m, "<"), ">")
            if v, ok := bindings[key]; ok && strings.TrimSpace(v) != "" {
                return v
            }
            return m
        })
        out = append(out, rewritten)
    }
    // fix: if --role-name got an ARN, extract just the role name
    out = fixRoleNameArg(out)
    return out
}

Execution flow

When you run clanker ask --apply, this happens:
1

Parse plan

Load and validate the JSON plan.
internal/maker/plan.go:31-82
func ParsePlan(raw string) (*Plan, error) {
    trimmed := strings.TrimSpace(raw)
    if trimmed == "" {
        return nil, fmt.Errorf("empty plan")
    }

    var p Plan
    if err := json.Unmarshal([]byte(trimmed), &p); err != nil {
        // Try accepting alternative shapes the LLM sometimes returns
        wrapped, wrapErr := parsePlanFromAlternativeShapes(trimmed)
        if wrapErr == nil {
            p = *wrapped
        } else {
            return nil, err
        }
    }

    if len(p.Commands) == 0 {
        return nil, fmt.Errorf("plan has no commands")
    }

    // Normalize args
    for i := range p.Commands {
        p.Commands[i].Args = normalizeArgs(p.Commands[i].Args)
        if len(p.Commands[i].Args) == 0 {
            return nil, fmt.Errorf("command %d has empty args", i)
        }
    }

    return &p, nil
}
2

Resolve AWS context

Determine profile, region, and account ID.
3

Execute commands sequentially

For each command:
  1. Substitute placeholders
  2. Run the AWS/GCP/Azure/Cloudflare CLI command
  3. Learn bindings from output (via produces field)
  4. Handle failures with AI-powered retries
4

Wait for async operations

CloudFormation stacks and other long-running operations are automatically polled until completion.
5

Post-deployment fixes

If targets are unhealthy or services fail to start, Clanker runs diagnostics via SSM and applies remediation automatically.
for idx, cmdSpec := range plan.Commands {
    if resumeFromIndex > 0 && idx < resumeFromIndex {
        _, _ = fmt.Fprintf(opts.Writer, "[maker][checkpoint] skipping already-completed command %d/%d\n", idx+1, len(plan.Commands))
        continue
    }
    _, _ = fmt.Fprintf(opts.Writer, "[maker][checkpoint] start command %d/%d\n", idx+1, len(plan.Commands))

    if err := validateCommand(cmdSpec.Args, opts.Destroyer); err != nil {
        _ = maybeSwarmDiagnose(ctx, opts, "preflight: command rejected", cmdSpec.Args, err.Error(), bindings)
        return fmt.Errorf("command %d rejected: %w", idx+1, err)
    }

    args := make([]string, 0, len(cmdSpec.Args)+6)
    args = append(args, cmdSpec.Args...)
    args = substituteAccountID(args, accountID)
    args = applyPlanBindings(args, bindings)

    // ... (handle special cases like Lambda zip, EC2 user-data, etc.)

    awsArgs := buildAWSExecArgs(args, opts, opts.Writer)

    _, _ = fmt.Fprintf(opts.Writer, "[maker] running %d/%d: %s\n", idx+1, len(plan.Commands), formatAWSArgsForLog(awsArgs))

    out, runErr := runAWSCommandStreaming(ctx, awsArgs, zipBytes, opts.Writer)
    if runErr != nil {
        if handled, handleErr := handleAWSFailure(ctx, plan, opts, idx, args, awsArgs, zipBytes, out, runErr, remediationAttempted, bindings, healPolicy, healRuntime); handled {
            if handleErr != nil {
                return handleErr
            }
            // Retry succeeded
            continue
        }
        return fmt.Errorf("aws command %d failed: %w", idx+1, runErr)
    }

    // Learn placeholder bindings from successful command outputs.
    learnPlanBindingsFromProduces(cmdSpec.Produces, out, bindings)
    learnPlanBindings(args, out, bindings)
    bindings["CHECKPOINT_LAST_SUCCESS_INDEX"] = strconv.Itoa(idx + 1)

    // CloudFormation is async. If we just created/updated a stack, wait for it to complete.
    if len(args) >= 2 && args[0] == "cloudformation" && (args[1] == "create-stack" || args[1] == "update-stack") {
        stackName := strings.TrimSpace(flagValue(args, "--stack-name"))
        if stackName != "" {
            status, details, waitErr := waitForCloudFormationStackTerminal(ctx, opts, stackName, opts.Writer)
            if waitErr != nil {
                return fmt.Errorf("cloudformation wait failed for %s: %w", stackName, waitErr)
            }
            if !isCloudFormationStackSuccess(status) {
                // ... (handle failure with AI retry)
            }
        }
    }

    _, _ = fmt.Fprintf(opts.Writer, "[maker][checkpoint] success command %d/%d\n", idx+1, len(plan.Commands))
}

AI-powered error handling

When a command fails, Clanker uses agentic fixes to automatically retry:

How it works

  1. Analyze failure: Send the error output to an LLM
  2. Propose fix: LLM returns a agenticFix JSON object
  3. Apply fix: Execute pre-commands, update bindings, or rewrite args
  4. Retry: Run the fixed command
  5. Exponential backoff: Retry up to 3 times with increasing delays
type agenticFix struct {
    // RewrittenArgs: if non-nil, use these args instead of the original
    RewrittenArgs []string `json:"rewritten_args,omitempty"`
    // PreCommands: commands to run before retrying the original
    PreCommands []Command `json:"pre_commands,omitempty"`
    // Bindings: new placeholder bindings discovered
    Bindings map[string]string `json:"bindings,omitempty"`
    // Skip: if true, skip this command (it's already done or not needed)
    Skip bool `json:"skip,omitempty"`
    // Notes: explanation
    Notes []string `json:"notes,omitempty"`
}

Common fixes

ErrorAI Fix
AlreadyExistsExceptionSet skip: true
InvalidParameterValue: <PLACEHOLDER>Provide bindings or rewritten_args
ResourceNotFoundExceptionAdd pre_commands to create missing resource
AccessDeniedExceptionAdd IAM policy via pre_commands
Invalid role ARN syntaxExtract role name from ARN in rewritten_args

Checkpointing

Maker mode supports resumable execution via durable checkpoints:
  • Automatic: Checkpoints are saved after each successful command
  • Resume: If a plan fails mid-execution, re-run --apply to resume from the last checkpoint
  • Storage: Checkpoints are stored in ~/.clanker/checkpoints/
# First attempt fails at command 5
clanker ask --apply < plan.json
# ... commands 1-4 succeed, command 5 fails

# Fix the issue and resume
clanker ask --apply < plan.json
# ... commands 1-4 skipped (already completed), starts at command 5

Multi-cloud support

Maker mode supports multiple cloud providers:
Uses the AWS CLI (aws).
clanker ask --maker "Create an S3 bucket" --aws
Provider detection is automatic if your question mentions AWS services.

Examples

Create a VPC with subnets

clanker ask --maker "Create a VPC with public and private subnets in us-east-1" > vpc-plan.json
clanker ask --apply < vpc-plan.json

Deploy a Lambda function

clanker ask --maker "Create a Lambda function that processes S3 events" > lambda-plan.json
clanker ask --apply < lambda-plan.json

Destroy resources

clanker ask --maker --destroyer "Delete the VPC and all associated resources" > destroy-plan.json
clanker ask --apply < destroy-plan.json
Destructive mode: The --destroyer flag allows deletion commands. Always review plans before applying!

Next steps

Multi-cloud

Learn about multi-cloud provider support

CLI reference

Full CLI options for maker mode

Build docs developers (and LLMs) love