{"slug": "stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on", "title": "Stop Hardcoding Prompts: Prompt Management and Prompt Flows on Amazon Bedrock (Hands-On)", "summary": "Amazon Bedrock's Prompt Management and Prompt Flows features let developers manage prompts as versioned AWS resources and orchestrate multi-step LLM pipelines without glue code. A hands-on guide demonstrates building a customer-support pipeline that classifies inquiries, routes them, and answers with the appropriate model, using Amazon Nova Micro for cheap tasks and Claude for technical questions. The approach reduces prompt sprawl and enables cost optimization by routing cheap traffic away from expensive models.", "body_md": "If your team is building LLM applications, your prompts are probably scattered across application code as string literals. Changing a single word means a code change, a review, and a redeploy — and nobody can tell which prompt version is actually running in production. As LLM apps move from prototypes to production systems, prompt sprawl becomes a real operational problem.\n\nIn this hands-on, we'll fix that with two Amazon Bedrock features — **Prompt Management** and **Prompt Flows** — by building a customer-support pipeline that classifies an inquiry, routes it, and answers it with the right model. Entirely from the AWS CLI, every command included.\n\nPrefer video? This entire hands-on is also on YouTube:\n\n```\nCustomer inquiry\n      │\n      ▼\n [Classifier prompt]  ← Amazon Nova Micro (cheap, temperature 0)\n      │\n      ▼\n   [Router]  ── TECH ──▶ [Tech answer prompt]    ← Claude (accurate)\n      │\n      └── everything else ──▶ [General answer prompt] ← Nova Micro (cheap)\n```\n\nNote the cost design: the classification and general answers run on Amazon Nova Micro (fast and inexpensive), while only genuinely technical questions reach Claude. Routing cheap traffic away from your most expensive model is one of the most practical cost optimizations in production LLM systems.\n\nPrompt Management turns a prompt into a **first-class AWS resource**. Instead of a string in your code, a prompt becomes an object with its own ARN, and it carries everything needed to run it:\n\n`{{question}}`\n\n— filled in at invocation time`modelId`\n\n)The consequences matter more than the mechanics. Your application code no longer contains prompt text at all — it references a prompt ARN. Prompt engineers can iterate on the DRAFT without touching application code, while production keeps calling a pinned version. Swapping the underlying model for a prompt is a configuration change, not a code change.\n\nPrompt Flows is Bedrock's **serverless orchestration layer for multi-step LLM pipelines**. A flow is a graph of nodes — Input, Prompt, Condition, Output, and others — connected by data and conditional edges. The flow engine executes the graph for you: no Lambda glue code, no Step Functions state machine to maintain.\n\nFlows also get the same lifecycle treatment as prompts: you version a flow and expose it through **aliases** (e.g. a `prod`\n\nalias pinned to version 1), so you can rewire the pipeline behind a stable identifier without redeploying callers.\n\nTogether, the two features give you something teams usually hand-roll: a prompt registry with versioning, plus a managed execution engine with routing — both callable through standard AWS APIs and IAM.\n\n`us-east-1`\n\n`jq`\n\nand `python3`\n\n(with `boto3`\n\n) installedBedrock models are updated frequently — check the console and use the latest versions.\n\n```\nexport CLAUDE_MODEL=\"us.anthropic.claude-haiku-4-5-20251001-v1:0\"\nexport NOVA_MODEL=\"us.amazon.nova-micro-v1:0\"\nexport REGION=us-east-1\n```\n\nFirst, the classifier. Temperature 0 and a 10-token cap: we want a deterministic label, nothing else.\n\n```\nCLASSIFIER_ID=$(aws bedrock-agent create-prompt --region $REGION \\\n  --name customer-classifier \\\n  --description \"Classify customer inquiries as TECH or GENERAL\" \\\n  --default-variant v1 \\\n  --variants '[{\n    \"name\":\"v1\",\n    \"templateType\":\"TEXT\",\n    \"templateConfiguration\":{\"text\":{\n      \"text\":\"Classify the following customer inquiry into exactly one of the following. Respond with the label only and nothing else.\\n\\n- TECH: technical product issues, configuration, errors, how to use the product\\n- GENERAL: pricing, contracts, business hours, and other general questions\\n\\nInquiry: {{question}}\\n\\nLabel:\",\n      \"inputVariables\":[{\"name\":\"question\"}]\n    }},\n    \"modelId\":\"'\"$NOVA_MODEL\"'\",\n    \"inferenceConfiguration\":{\"text\":{\"temperature\":0.0,\"maxTokens\":10}}\n  }]' \\\n  --query 'id' --output text)\n\necho \"CLASSIFIER_ID=$CLASSIFIER_ID\"\n```\n\nNext, the technical answerer — this one runs on Claude, with a lower temperature for accuracy:\n\n```\nTECH_ID=$(aws bedrock-agent create-prompt --region $REGION \\\n  --name customer-tech-answer \\\n  --description \"Answer technical inquiries with Claude\" \\\n  --default-variant v1 \\\n  --variants '[{\n    \"name\":\"v1\",\n    \"templateType\":\"TEXT\",\n    \"templateConfiguration\":{\"text\":{\n      \"text\":\"You are a technical support representative for our products. Answer the following technical inquiry concisely and accurately.\\n\\nInquiry: {{question}}\\n\\nAnswer:\",\n      \"inputVariables\":[{\"name\":\"question\"}]\n    }},\n    \"modelId\":\"'\"$CLAUDE_MODEL\"'\",\n    \"inferenceConfiguration\":{\"text\":{\"temperature\":0.3,\"maxTokens\":400}}\n  }]' \\\n  --query 'id' --output text)\n\necho \"TECH_ID=$TECH_ID\"\n```\n\nAnd the general answerer, back on Nova Micro:\n\n```\nGENERAL_ID=$(aws bedrock-agent create-prompt --region $REGION \\\n  --name customer-general-answer \\\n  --description \"Answer general inquiries with Nova\" \\\n  --default-variant v1 \\\n  --variants '[{\n    \"name\":\"v1\",\n    \"templateType\":\"TEXT\",\n    \"templateConfiguration\":{\"text\":{\n      \"text\":\"You are a customer support representative for our company. Answer the following general inquiry politely and helpfully.\\n\\nInquiry: {{question}}\\n\\nAnswer:\",\n      \"inputVariables\":[{\"name\":\"question\"}]\n    }},\n    \"modelId\":\"'\"$NOVA_MODEL\"'\",\n    \"inferenceConfiguration\":{\"text\":{\"temperature\":0.5,\"maxTokens\":400}}\n  }]' \\\n  --query 'id' --output text)\n\necho \"GENERAL_ID=$GENERAL_ID\"\n```\n\nThree prompts, three independent lifecycles, two different models — and not a single line of application code yet.\n\nHere's the detail that surprises most people: the `Converse`\n\nAPI accepts a **prompt ARN as the model ID**. The prompt brings its own model and inference settings; you only supply the variables.\n\n```\nACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)\nCLASSIFIER_ARN=\"arn:aws:bedrock:$REGION:$ACCOUNT_ID:prompt/$CLASSIFIER_ID\"\n\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN\" \\\n  --prompt-variables '{\"question\":{\"text\":\"I cannot log in. I want to reset my password.\"}}' \\\n  | jq -r '.output.message.content[0].text'\n```\n\nExpected output: `TECH`\n\n```\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN\" \\\n  --prompt-variables '{\"question\":{\"text\":\"How much is the monthly fee?\"}}' \\\n  | jq -r '.output.message.content[0].text'\n```\n\nExpected output: `GENERAL`\n\nEvery prompt has an editable DRAFT. `create-prompt-version`\n\nfreezes the current DRAFT into an immutable numbered version:\n\n```\nCLASSIFIER_V1=$(aws bedrock-agent create-prompt-version --region $REGION \\\n  --prompt-identifier $CLASSIFIER_ID \\\n  --description \"Production version\" \\\n  --query version --output text)\n\necho \"CLASSIFIER_V1=$CLASSIFIER_V1\"   # -> 1\n\nTECH_V1=$(aws bedrock-agent create-prompt-version --region $REGION \\\n  --prompt-identifier $TECH_ID --query version --output text)\nGENERAL_V1=$(aws bedrock-agent create-prompt-version --region $REGION \\\n  --prompt-identifier $GENERAL_ID --query version --output text)\necho \"TECH_V1=$TECH_V1  GENERAL_V1=$GENERAL_V1\"\n```\n\nThe ARN convention does the rest. Append `:1`\n\nfor the pinned production version; omit the suffix to hit the DRAFT:\n\n```\n# Production: pinned to version 1\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN:$CLASSIFIER_V1\" \\\n  --prompt-variables '{\"question\":{\"text\":\"I want to reset my password\"}}' \\\n  | jq -r '.output.message.content[0].text'\n\n# Testing: the DRAFT, with whatever edits are in flight\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN\" \\\n  --prompt-variables '{\"question\":{\"text\":\"I want to reset my password\"}}' \\\n  | jq -r '.output.message.content[0].text'\n```\n\nLet's prove the isolation. Rewrite the DRAFT to add a third label, `BILLING`\n\n:\n\n```\naws bedrock-agent update-prompt --region $REGION \\\n  --prompt-identifier $CLASSIFIER_ID \\\n  --name customer-classifier \\\n  --default-variant v1 \\\n  --variants '[{\n    \"name\":\"v1\",\n    \"templateType\":\"TEXT\",\n    \"templateConfiguration\":{\"text\":{\n      \"text\":\"Classify the following customer inquiry into exactly one of the following. Respond with the label only and nothing else.\\n\\n- TECH: technical product issues, configuration, errors, how to use the product\\n- BILLING: pricing, invoices, payment methods\\n- GENERAL: everything else\\n\\nInquiry: {{question}}\\n\\nLabel:\",\n      \"inputVariables\":[{\"name\":\"question\"}]\n    }},\n    \"modelId\":\"'\"$NOVA_MODEL\"'\",\n    \"inferenceConfiguration\":{\"text\":{\"temperature\":0.0,\"maxTokens\":10}}\n  }]'\n```\n\nNow ask the same billing question twice:\n\n```\n# DRAFT — picks up the new behavior\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN\" \\\n  --prompt-variables '{\"question\":{\"text\":\"How much is the monthly fee?\"}}' \\\n  | jq -r '.output.message.content[0].text'\n# -> BILLING\n\n# Version 1 — production is untouched\naws bedrock-runtime converse --region $REGION \\\n  --model-id \"$CLASSIFIER_ARN:1\" \\\n  --prompt-variables '{\"question\":{\"text\":\"How much is the monthly fee?\"}}' \\\n  | jq -r '.output.message.content[0].text'\n# -> GENERAL\n```\n\nSame prompt resource, two behaviors, zero risk to production. This is the workflow Prompt Management exists for.\n\nNow we wire the three prompts into one pipeline.\n\nA flow runs under its own IAM role, which needs permission to invoke models and read the prompts:\n\n```\ncat > /tmp/flow-trust.json << 'EOF'\n{\n  \"Version\":\"2012-10-17\",\n  \"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"bedrock.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]\n}\nEOF\n\nFLOW_ROLE_ARN=$(aws iam create-role \\\n  --role-name bedrock-flow-role \\\n  --assume-role-policy-document file:///tmp/flow-trust.json \\\n  --query 'Role.Arn' --output text)\n\ncat > /tmp/flow-policy.json << 'EOF'\n{\n  \"Version\":\"2012-10-17\",\n  \"Statement\":[\n    {\"Effect\":\"Allow\",\"Action\":[\"bedrock:InvokeModel\",\"bedrock:Converse\",\"bedrock:GetPrompt\",\"bedrock:RenderPrompt\",\"bedrock:GetInferenceProfile\"],\"Resource\":\"*\"}\n  ]\n}\nEOF\n\naws iam put-role-policy \\\n  --role-name bedrock-flow-role \\\n  --policy-name bedrock-flow-inline \\\n  --policy-document file:///tmp/flow-policy.json\n\necho \"FLOW_ROLE_ARN=$FLOW_ROLE_ARN\"\nsleep 10   # wait for the role to propagate\n```\n\n(For production, scope `Resource`\n\ndown to the specific prompt and model ARNs.)\n\nA flow definition has two halves: **nodes** (the boxes) and **connections** (the arrows). Ours has an Input node, the three Prompt nodes referencing the **pinned production versions** of our prompts, a Condition node for routing, and two Output nodes:\n\n```\nCLASSIFIER_PROD_ARN=\"$CLASSIFIER_ARN:$CLASSIFIER_V1\"\nTECH_PROD_ARN=\"arn:aws:bedrock:$REGION:$ACCOUNT_ID:prompt/$TECH_ID:$TECH_V1\"\nGENERAL_PROD_ARN=\"arn:aws:bedrock:$REGION:$ACCOUNT_ID:prompt/$GENERAL_ID:$GENERAL_V1\"\n\njq -n \\\n  --arg cls \"$CLASSIFIER_PROD_ARN\" \\\n  --arg tech \"$TECH_PROD_ARN\" \\\n  --arg gen \"$GENERAL_PROD_ARN\" \\\n'{\n  nodes: [\n    { name:\"FlowInputNode\", type:\"Input\",\n      configuration:{input:{}},\n      outputs:[{name:\"document\", type:\"String\"}] },\n\n    { name:\"Classifier\", type:\"Prompt\",\n      configuration:{prompt:{sourceConfiguration:{resource:{promptArn:$cls}}}},\n      inputs:[{name:\"question\", type:\"String\", expression:\"$.data\"}],\n      outputs:[{name:\"modelCompletion\", type:\"String\"}] },\n\n    { name:\"Router\", type:\"Condition\",\n      configuration:{condition:{conditions:[\n        {name:\"isTech\", expression:\"classification == \\\"TECH\\\"\"},\n        {name:\"default\"}\n      ]}},\n      inputs:[{name:\"classification\", type:\"String\", expression:\"$.data\"}] },\n\n    { name:\"TechAnswer\", type:\"Prompt\",\n      configuration:{prompt:{sourceConfiguration:{resource:{promptArn:$tech}}}},\n      inputs:[{name:\"question\", type:\"String\", expression:\"$.data\"}],\n      outputs:[{name:\"modelCompletion\", type:\"String\"}] },\n\n    { name:\"GeneralAnswer\", type:\"Prompt\",\n      configuration:{prompt:{sourceConfiguration:{resource:{promptArn:$gen}}}},\n      inputs:[{name:\"question\", type:\"String\", expression:\"$.data\"}],\n      outputs:[{name:\"modelCompletion\", type:\"String\"}] },\n\n    { name:\"TechOutputNode\", type:\"Output\",\n      configuration:{output:{}},\n      inputs:[{name:\"document\", type:\"String\", expression:\"$.data\"}] },\n\n    { name:\"GeneralOutputNode\", type:\"Output\",\n      configuration:{output:{}},\n      inputs:[{name:\"document\", type:\"String\", expression:\"$.data\"}] }\n  ],\n  connections: [\n    {name:\"i2c\", source:\"FlowInputNode\", target:\"Classifier\",         type:\"Data\",\n      configuration:{data:{sourceOutput:\"document\", targetInput:\"question\"}}},\n    {name:\"i2t\", source:\"FlowInputNode\", target:\"TechAnswer\",         type:\"Data\",\n      configuration:{data:{sourceOutput:\"document\", targetInput:\"question\"}}},\n    {name:\"i2g\", source:\"FlowInputNode\", target:\"GeneralAnswer\",      type:\"Data\",\n      configuration:{data:{sourceOutput:\"document\", targetInput:\"question\"}}},\n    {name:\"c2r\", source:\"Classifier\",    target:\"Router\",             type:\"Data\",\n      configuration:{data:{sourceOutput:\"modelCompletion\", targetInput:\"classification\"}}},\n    {name:\"r2t\", source:\"Router\",        target:\"TechAnswer\",         type:\"Conditional\",\n      configuration:{conditional:{condition:\"isTech\"}}},\n    {name:\"r2g\", source:\"Router\",        target:\"GeneralAnswer\",      type:\"Conditional\",\n      configuration:{conditional:{condition:\"default\"}}},\n    {name:\"t2o\", source:\"TechAnswer\",    target:\"TechOutputNode\",     type:\"Data\",\n      configuration:{data:{sourceOutput:\"modelCompletion\", targetInput:\"document\"}}},\n    {name:\"g2o\", source:\"GeneralAnswer\", target:\"GeneralOutputNode\",  type:\"Data\",\n      configuration:{data:{sourceOutput:\"modelCompletion\", targetInput:\"document\"}}}\n  ]\n}' > /tmp/flow-def.json\n\ncat /tmp/flow-def.json | jq '.nodes[].name, .connections[].name'\n```\n\nTwo details worth noticing:\n\n`Data`\n\nconnections, but `TechAnswer`\n\nand `GeneralAnswer`\n\nonly `Conditional`\n\nedge from the Router fires. The Condition node gates execution, not data.`classification == \"TECH\"`\n\n) compares the classifier's raw output — which is exactly why we forced the classifier to answer with the label only.\n\n```\nFLOW_ID=$(aws bedrock-agent create-flow --region $REGION \\\n  --name customer-support-flow \\\n  --description \"Classify -> route -> answer\" \\\n  --execution-role-arn \"$FLOW_ROLE_ARN\" \\\n  --definition file:///tmp/flow-def.json \\\n  --query 'id' --output text)\n\necho \"FLOW_ID=$FLOW_ID\"\n\n# Compile the flow into an executable state\naws bedrock-agent prepare-flow --region $REGION \\\n  --flow-identifier $FLOW_ID\n```\n\n`invoke_flow`\n\nstreams events, so we test from Python. `TSTALIASID`\n\nis the built-in alias that always points at the working draft of the flow:\n\n``` python\nimport boto3\n\nclient = boto3.client(\"bedrock-agent-runtime\", region_name=\"us-east-1\")\n\ndef ask(question):\n    resp = client.invoke_flow(\n        flowIdentifier=\"<FLOW_ID>\",\n        flowAliasIdentifier=\"TSTALIASID\",\n        inputs=[{\n            \"nodeName\": \"FlowInputNode\",\n            \"nodeOutputName\": \"document\",\n            \"content\": {\"document\": question}\n        }]\n    )\n    for event in resp[\"responseStream\"]:\n        if \"flowOutputEvent\" in event:\n            out = event[\"flowOutputEvent\"]\n            print(f\"  [Output node: {out['nodeName']}]\")\n            print(\"  \" + out[\"content\"][\"document\"].replace(\"\\n\", \"\\n  \"))\n\nprint(\"=== Technical question ===\")\nask(\"The app crashes as soon as I launch it. What could be the cause?\")\n\nprint()\nprint(\"=== General question ===\")\nask(\"What are the support center's business hours?\")\n```\n\nThe technical question comes back from `TechOutputNode`\n\n(answered by Claude), the business-hours question from `GeneralOutputNode`\n\n(answered by Nova). The routing works.\n\nFlows version exactly like prompts — and aliases give callers a stable name:\n\n```\nFLOW_V1=$(aws bedrock-agent create-flow-version --region $REGION \\\n  --flow-identifier $FLOW_ID \\\n  --description \"Production release v1\" \\\n  --query version --output text)\n\necho \"FLOW_V1=$FLOW_V1\"   # -> 1\n\nPROD_ALIAS_ID=$(aws bedrock-agent create-flow-alias --region $REGION \\\n  --flow-identifier $FLOW_ID \\\n  --name prod \\\n  --routing-configuration '[{\"flowVersion\":\"'\"$FLOW_V1\"'\"}]' \\\n  --query id --output text)\n\necho \"PROD_ALIAS_ID=$PROD_ALIAS_ID\"\n```\n\nProduction callers use the `prod`\n\nalias and never change, even when you later repoint the alias at version 2:\n\n``` python\nimport boto3\nclient = boto3.client(\"bedrock-agent-runtime\", region_name=\"us-east-1\")\nresp = client.invoke_flow(\n    flowIdentifier=\"<FLOW_ID>\",\n    flowAliasIdentifier=\"<PROD_ALIAS_ID>\",\n    inputs=[{\n        \"nodeName\": \"FlowInputNode\",\n        \"nodeOutputName\": \"document\",\n        \"content\": {\"document\": \"How do I get a receipt issued?\"}\n    }]\n)\nfor event in resp[\"responseStream\"]:\n    if \"flowOutputEvent\" in event:\n        out = event[\"flowOutputEvent\"]\n        print(f\"[{out['nodeName']}]\")\n        print(out[\"content\"][\"document\"])\n```\n\nDelete child resources before parents:\n\n``` php\n# Flow (alias -> version -> flow)\naws bedrock-agent delete-flow-alias --region $REGION \\\n  --flow-identifier $FLOW_ID --alias-identifier $PROD_ALIAS_ID\naws bedrock-agent delete-flow-version --region $REGION \\\n  --flow-identifier $FLOW_ID --flow-version $FLOW_V1\naws bedrock-agent delete-flow --region $REGION \\\n  --flow-identifier $FLOW_ID --skip-resource-in-use-check\n\n# Prompts\nfor P in $CLASSIFIER_ID $TECH_ID $GENERAL_ID; do\n  aws bedrock-agent delete-prompt --region $REGION --prompt-identifier $P\ndone\n\n# IAM\naws iam delete-role-policy --role-name bedrock-flow-role --policy-name bedrock-flow-inline\naws iam delete-role --role-name bedrock-flow-role\n```\n\nPrompt Management and Prompt Flows are not just convenience features — they move your prompts and your LLM pipeline out of application code and into versioned, IAM-governed AWS resources. The DRAFT/version split gives prompt engineering a safe test-in-production workflow, and flow aliases let you re-architect a pipeline behind a stable endpoint. If you run LLM workloads on AWS, I recommend validating this pattern in your own environment.\n\n**About the author**\n\nMaruchin Tech — 12x AWS Certified | Cloud & AI for manufacturing and supply chain (AWS / Google Cloud / Azure) | Udemy instructor (100K+ students)\n\n🎥 Video version of this hands-on:\n\n[https://youtu.be/iyK_xK3G-i0](https://youtu.be/iyK_xK3G-i0)\n\n📚 Full course — AWS Certified Generative AI Developer Professional (AIP-C01) Exam Prep:\n\n[https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/](https://www.udemy.com/course/aws-certified-generative-ai-developer-professional-exam-prep/)\n\n👨🏫 All my courses (Udemy profile):\n\n[https://www.udemy.com/user/maruchin-tech-2/](https://www.udemy.com/user/maruchin-tech-2/)\n\n🎫 Monthly discount coupons:\n\n[https://www.youtube.com/@MaruchinTech-cloud/community](https://www.youtube.com/@MaruchinTech-cloud/community)", "url": "https://wpnews.pro/news/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on", "canonical_source": "https://dev.to/maruchin_tech_555/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-hands-on-3pad", "published_at": "2026-08-21 17:16:04+00:00", "updated_at": "2026-08-21 17:44:55.201510+00:00", "lang": "en", "topics": ["artificial-intelligence", "large-language-models", "ai-products", "ai-infrastructure", "developer-tools"], "entities": ["Amazon Bedrock", "Amazon Nova Micro", "Claude", "AWS CLI"], "alternates": {"html": "https://wpnews.pro/news/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on", "markdown": "https://wpnews.pro/news/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on.md", "text": "https://wpnews.pro/news/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on.txt", "jsonld": "https://wpnews.pro/news/stop-hardcoding-prompts-prompt-management-and-prompt-flows-on-amazon-bedrock-on.jsonld"}}