![[アップデート] AWS Step FunctionsのワークフローをAgentCore Gateway経由でAIエージェントのツールとして使えるようになりました!](https://devio2024-media.developers.io/image/upload/f_auto,q_auto,w_3840/f_auto,q_auto/v1790875048/eyecatch/edq4dfptctqlcwjhffhs.png)
[アップデート] AWS Step FunctionsのワークフローをAgentCore Gateway経由でAIエージェントのツールとして使えるようになりました!
はじめに
こんにちは、腰痛がひどいコンサル部の神野です。
AWS Step Functionsのワークフローを、Amazon Bedrock AgentCore Gateway経由でAIエージェントのツールとして使えるようになりました!
AgentCore Gatewayの連携がガンガン増えていきますね!本記事では変更点をまとめて実際に触ってみます!!
また、同じアップデートをふくちさん(@har1101mony)が詳しく検証されています!10個の連携できる操作の整理やタスクトークンを使ったエージェント同士の連携、日本語まわりのハマりどころなどが書かれているのでぜひご参照ください!
本記事ではそちらと重なる部分は軽めにして、動かしてみた手触りやCedar周りなどふかぼっていければと思います!
主な変更点
ワークフローがそのままツールになる
AgentCore GatewayにStep Functionsのコネクタが追加されました!ターゲットを作るときに連携する操作とステートマシンを選ぶとGatewayがツールの定義を作ります。エージェントからの呼び出しもGatewayがStep FunctionsのAPIへつないでくれます。

これまでもSDKでStep FunctionsのAPIを直接呼び出せましたが、Gatewayを挟むとツール定義・権限・動かせるステートマシンをGateway側でまとめて管理できてシームレスに連携できるのが嬉しいですね!
Gatewayに集約すると何が嬉しいのかは下記記事でDeepDiveしているので必要に応じてみていただけると!
エージェント側は「いつワークフローを動かすか」と「何を入力するか」を自律的に判断して、処理の実行順序や業務ルールなど確実に守らせたいものをステートマシン側で制御する棲み分けで活用するイメージですかね!
連携できる操作は10個
コネクタで連携できるStep Functionsの操作は10個です。ステートマシンを対象にする StartExecution や StartSyncExecution などは、ターゲットの設定でステートマシンのARNを固定します。固定したARNはツールの引数に出てこないので、エージェントが別のステートマシンを動かすことはできません。
連携できる操作の一覧は公式ドキュメントにまとまっています。
StandardとExpressで使える操作が違う
一部の操作はワークフローの種類によって使えないので軽く押さえておきましょう。
| 操作 | 使える種類 |
|---|---|
StartSyncExecution |
Expressのみ(同期で結果まで待機) |
ListExecutions・DescribeExecution・GetExecutionHistory・StopExecution |
Standardのみ |
StartExecution・DescribeStateMachine |
両方 |
SendTaskSuccess・SendTaskFailure・SendTaskHeartbeat |
Standardのみ(Expressはコールバックの待機に対応していない) |
料金はStep FunctionsとAgentCore Gatewayの通常料金が適用されます。
Gatewayが使えるすべてのリージョンで利用できます!
もちろんコンソールからも作れる
Gatewayのターゲット作成画面でコネクターの「Step Functions」を選ぶとステートマシンと公開する操作を選べます。

コンソールから手動で作ることもできますが、今回はCDKで作ります!!
やってみた
構成
私が大好きなスーパーマーケットを題材にします。
発注業務をイメージしてみます。
発注担当のエージェントが在庫を確認して発注し、合計が3万円を超える場合は店長がメールで承認する流れです。

Gatewayには役割ごとに2つのターゲットを作りました。
| ターゲット | 操作 | 接続先 |
|---|---|---|
order |
発注の開始と状況確認 | 発注ワークフロー(Standard) |
stock |
在庫の照会 | 在庫照会ワークフロー(Express) |
ここで作る発注ワークフローは発注数と単価から合計を計算して分岐します。ざっとこんなような特徴です。
- 3万円以下ならそのまま発注を確定し、超える場合はSNSで店長に承認依頼のメールを送って待ちます。
- 店長がメールのリンクから承認すると承認用のLambdaが
SendTaskSuccessでワークフローを再開させます。 - 在庫照会ワークフローはDynamoDBの在庫テーブルから、商品名をキーに在庫数・単価・1日の販売数を取得して返します。
- GatewayにはPolicy Engine(Cedar)も付けて、発注の取り消しを店長のロールだけに許可します。
発注ワークフローの流れはこんな感じです。

あくまで今回のアップデートを試すサンプルで実際に使えるものではないのでご承知おきください!
AWSのリソースはCDKで作り、エージェントはStrands Agentsで作ります。サンプルコードはGitHubにアップロードしています!
前提
今回使用した環境は下記です。
| 項目 | 内容 |
|---|---|
| リージョン | us-east-1(バージニア北部) |
| Node.js / pnpm | 26.13.0 / 12.16.1 |
| aws-cdk-lib / aws-cdk | 2.275.0 / 2.1146.1 |
| Python / uv | 3.15.2 / 0.13.6 |
| 承認用Lambdaのランタイム | Python 3.14 |
| strands-agents | 1.62.0 |
| mcp-proxy-for-aws | 1.7.0 |
| モデル | Claude Haiku 5.5(us.anthropic.claude-haiku-5-5) |
なお、2026年10月30日時点ではGateway経由で渡した入力の日本語が「?」に化けます。今回のエージェントでは、ワークフローに渡すJSONの日本語を \uXXXX の形にエスケープしてから送っています。
CDKでリソースを作る
サンプルコードをcloneして、CDKの依存関係を入れます。
git clone https://github.com/yuu551/agentcore-gateway-stepfunctions-tools.git
cd agentcore-gateway-stepfunctions-tools/cdk
pnpm install
GatewayはL2の Gateway で書けますが、Step FunctionsのコネクタのターゲットはL2にまだないのでL1の CfnGatewayTarget で書きます。CloudFormationの AWS::BedrockAgentCore::GatewayTarget は Connector に対応しているので問題なくIaCで管理できます!
// Step Functionsのコネクタターゲット(L2がないのでL1で書く)
const connectorTarget = (name: string, configurations: agentcore.CfnGatewayTarget.ConnectorConfigurationProperty[]) =>
new agentcore.CfnGatewayTarget(this, `${name}Target`, {
gatewayIdentifier: gateway.gatewayId,
name,
targetConfiguration: {
mcp: { connector: { source: { connectorId: "states" }, configurations } },
},
credentialProviderConfigurations: [{ credentialProviderType: "GATEWAY_IAM_ROLE" }],
});
connectorTarget("order", [
{ name: "StartExecution", parameterValues: { stateMachineArn: order.stateMachineArn } },
{ name: "ListExecutions", parameterValues: { stateMachineArn: order.stateMachineArn } },
{ name: "DescribeStateMachine", parameterValues: { stateMachineArn: order.stateMachineArn } },
{ name: "DescribeExecution" },
{ name: "GetExecutionHistory" },
{ name: "StopExecution" },
]);
connectorTarget("stock", [
{ name: "StartSyncExecution", parameterValues: { stateMachineArn: stock.stateMachineArn } },
]);
コネクタIDに states を指定し、configurations に連携する操作を記載します。
ステートマシンを固定する操作には parameterValues.stateMachineArn を指定します。
在庫照会ワークフローは dynamodb:getItem でDynamoDBの在庫テーブルを直接読みにいく作りとしています。
"GetStock": {
"Type": "Task",
"Resource": "arn:aws:states:::dynamodb:getItem",
"Arguments": {
"TableName": "${TABLE_NAME}",
"Key": {
"item": {
"S": "{% $states.input.item %}"
}
}
},
"Output": "{% $exists($states.result.Item) ? {'item': $states.result.Item.item.S, 'stock': $number($states.result.Item.stock.N), 'unitPrice': $number($states.result.Item.unitPrice.N), 'dailySales': $number($states.result.Item.dailySales.N)} : {'item': $states.input.item, 'status': 'NOT_FOUND'} %}",
"End": true
}
在庫テーブルには、デプロイ時に AwsCustomResource で牛乳・卵・米5kgの3件を入れています。
承認依頼のメールは、承認待ちのタスクで sns:publish.waitForTaskToken を使って送り、本文に承認用LambdaのURLとタスクトークンを入れています。
"WaitForApproval": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish.waitForTaskToken",
"TimeoutSeconds": 86400,
"Arguments": {
"TopicArn": "${TOPIC_ARN}",
"Subject": "Purchase order approval request",
"Message": "{% '発注の承認をお願いします。\n\n商品: ' & $states.input.item & '\n数量: ' & $states.input.quantity & '\n合計: ' & $states.input.quantity * $states.input.unitPrice & '円\n理由: ' & $states.input.reason & '\n\n承認・却下はこちらから\n' & '${APPROVAL_URL}?token=' & $encodeUrlComponent($states.context.Task.Token) %}"
},
"Output": "{% $merge([$states.input, {'approval': $states.result}]) %}",
"Next": "IsApproved"
}
承認待ちには TimeoutSeconds で24時間の期限を付けて、古いリンクでは承認できないようにしています。
承認用のLambdaはリンクを開くと承認・却下のボタンを表示し、ボタンが押されたら SendTaskSuccess で結果を返します。
流石にノーガードは嫌だなと思い簡易的ではありますが承認ページにはBasic認証を付けています。パスワードはCDKでSecrets Managerに生成し、Lambdaでチェックするような処理です。実際に使う際はしっかりと作り込みましょうね!
import base64
import hmac
import html
import json
import os
from urllib.parse import parse_qs
import boto3
sfn = boto3.client("stepfunctions")
PASSWORD = boto3.client("secretsmanager").get_secret_value(SecretId=os.environ["PASSWORD_SECRET_ARN"])["SecretString"]
PAGE = """<!doctype html><meta charset="utf-8"><title>発注の承認</title>
<p>今回の検証用の簡素なページです。本番用ではありません。</p>
<form method="post"><input type="hidden" name="token" value="{token}">
<button name="action" value="approve">承認する</button>
<button name="action" value="reject">却下する</button></form>"""
def handler(event, context):
# 店長のパスワードを知っている人だけが開けるようにする(Basic認証)
if not authorized(event["headers"].get("authorization", "")):
return {"statusCode": 401, "headers": {"WWW-Authenticate": 'Basic realm="approval"'}, "body": "Unauthorized"}
# リンクを開いただけでは承認せず、ボタンが押されたら結果を返す
if event["requestContext"]["http"]["method"] == "GET":
token = event["queryStringParameters"]["token"]
return page(PAGE.format(token=html.escape(token)))
body = event.get("body", "")
if event.get("isBase64Encoded"):
body = base64.b64decode(body).decode()
form = {k: v[0] for k, v in parse_qs(body).items()}
approved = form["action"] == "approve"
sfn.send_task_success(taskToken=form["token"], output=json.dumps({"approved": approved}))
return page("<p>承認しました</p>" if approved else "<p>却下しました</p>")
def authorized(header):
if not header.startswith("Basic "):
return False
try:
_, _, password = base64.b64decode(header.removeprefix("Basic ")).decode().partition(":")
except ValueError:
return False
return hmac.compare_digest(password, PASSWORD)
def page(body):
return {"statusCode": 200, "headers": {"Content-Type": "text/html; charset=utf-8"}, "body": body}
承認依頼の宛先メールアドレスを渡してデプロイし、出力されたGatewayのURLなどをリポジトリのルートの .env に書き出します。
pnpm exec cdk deploy -c email=<店長のメールアドレス> --outputs-file outputs.json
cd ..
jq -r '.SfnGatewayToolsStack | "GATEWAY_URL=\(.GatewayUrl)\nSTOCK_ARN=\(.StockArn)"' cdk/outputs.json > .env
デプロイ後にAWSから届く「Subscription Confirmation」のメールで、Confirm subscriptionを押しておきます。

承認ページのパスワードは、出力された PasswordSecretArn のシークレットから取得します。ユーザー名は任意です。
aws secretsmanager get-secret-value --secret-id <PasswordSecretArnの値> \
--query SecretString --output text
ツールを確認する
エージェント側の実装を用意していきます。リポジトリのルートで依存関係を入れます。
uv sync
set -a; source .env; set +a
Pythonを実行するIAMユーザーまたはロールには、作成したGatewayへの bedrock-agentcore:InvokeGateway を許可しておきましょう!
tools/list でツールを確認します。
list_tools.py
import asyncio
import json
import os
from mcp import ClientSession
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
async def main():
async with aws_iam_streamablehttp_client(
endpoint=os.environ["GATEWAY_URL"],
aws_region="us-east-1",
aws_service="bedrock-agentcore",
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
for tool in tools.tools:
print(tool.name)
print(" ", json.dumps(tool.inputSchema, ensure_ascii=False))
print(" ", tool.description)
asyncio.run(main())
uv run list_tools.py
order___StartExecution
{"type": "object", "properties": {"input": {"description": "The string that contains the JSON input data of the execution. ...", "type": "string"}}}
Starts a state machine execution. Returns the executionArn to use with DescribeExecution, GetExecutionHistory and StopExecution.
stock___StartSyncExecution
{"type": "object", "properties": {"input": {"description": "The string that contains the JSON input data of the execution. ...", "type": "string"}}}
Starts a Synchronous Express state machine execution and waits for it to complete. Express workflows only; fails on a Standard state machine.
ツール名は「ターゲット名___操作名」になり、StartExecution の引数は input になっていますね。固定したステートマシンのARNは見えなくなっていました。
説明文は操作の汎用的な説明なので、ワークフローが何をするかや input に入れるJSONの形はツールからは分かりません。このあたりはエージェントのシステムプロンプトで伝えたりスキルにしてって感じですかね。
発注担当のエージェントを作る
Gatewayへの接続とツールの絞り込み、日本語のエスケープを gateway_tools.py に実装しています。
エスケープはStrandsのフックで行い、ツールを呼び出す直前に input のJSONをデコードし、json.dumps のデフォルト(ensure_ascii=True)で再エンコードしています。
import json
import os
import re
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
from strands.hooks import BeforeToolCallEvent
from strands.tools.mcp import MCPClient
def gateway_client(pattern: str) -> MCPClient:
# Gatewayのツールのうち、名前がpatternに合うものだけをエージェントに渡す
return MCPClient(
lambda: aws_iam_streamablehttp_client(
endpoint=os.environ["GATEWAY_URL"],
aws_region="us-east-1",
aws_service="bedrock-agentcore",
),
tool_filters={"allowed": [re.compile(pattern)]},
)
def escape_non_ascii(event: BeforeToolCallEvent):
args = event.tool_use["input"]
print(f"\n[{event.tool_use['name']}] {json.dumps(args, ensure_ascii=False)}")
# ワークフローに渡すJSONの日本語を \uXXXX 形式にしてから送る
if isinstance(args.get("input"), str):
args["input"] = json.dumps(json.loads(args["input"]))
発注担当のエージェントには stock と order のツールを渡し、システムプロンプトで入力の形を伝えます。
import sys
from strands import Agent
from strands.hooks import BeforeToolCallEvent
from gateway_tools import escape_non_ascii, gateway_client
SYSTEM_PROMPT = """あなたはスーパーの発注担当です。
- 在庫は stock___StartSyncExecution に {"item": "商品名"} を渡して確認します
- 発注は order___StartExecution に {"item": "商品名", "quantity": 発注数, "unitPrice": 単価, "reason": "発注理由"} を渡して開始します
- 発注を開始したら order___DescribeExecution で結果を確認します
"""
agent = Agent(
model="us.anthropic.claude-haiku-5-5",
tools=[gateway_client(r"^(stock|order)___")],
system_prompt=SYSTEM_PROMPT,
)
agent.hooks.add_callback(BeforeToolCallEvent, escape_non_ascii)
agent(sys.argv[1])
こんなところでフックが役立つとは・・・
牛乳を発注する
まずは承認が要らない金額の発注から試してみます!
uv run store_agent.py "牛乳の在庫が少ないので、明日の分を発注しておいて"
まず牛乳の在庫を確認します。
[stock___StartSyncExecution] {"input": "{\"item\": \"牛乳\"}"}
牛乳の在庫は3本、1日の販売数は40本、単価は180円でした。明日の販売分として40本を発注します。
[order___StartExecution] {"input": "{\"item\": \"牛乳\", \"quantity\": 40, \"unitPrice\": 180, \"reason\": \"在庫残り3本のため、明日分(1日の販売数40本)を補充\"}"}
[order___DescribeExecution] {"executionArn": "arn:aws:states:us-east-1:123456789012:execution:PurchaseOrderWorkflow:4daf7182-...", "includedData": "ALL_DATA"}
牛乳の発注が完了しました。
- **商品**: 牛乳
- **発注数**: 40本
- **単価**: 180円
- **合計**: 7,200円
- **ステータス**: ORDERED(発注済み)
Expressにした在庫照会を StartSyncExecution で呼び出して在庫と単価を確認し、そこから発注数を決めてStandardの発注ワークフローを開始しています。合計7,200円で3万円以下なので、そのまま ORDERED になりました!
StartExecution は実行ARNを返す非同期処理のため、エージェントは続けて DescribeExecution を呼び出して実行結果を確認しています。
米を1週間分発注する
次は承認が必要な金額の発注を試してみます!
uv run store_agent.py "米5kgが残りわずかなので、1週間分を発注して"
在庫を確認します。
[stock___StartSyncExecution] {"input": "{\"item\": \"米5kg\"}"}
在庫は2個、1日の販売数は8個、単価は2,980円でした。1週間分は8×7=56個なので、手持ちの2個を引いて54個を発注します。
[order___StartExecution] {"input": "{\"item\": \"米5kg\", \"quantity\": 54, \"unitPrice\": 2980, \"reason\": \"残りわずかのため1週間分を発注(1日販売8個×7日=56個、在庫2個を差し引いて54個)\"}"}
[order___DescribeExecution] {...}
米5kgを54個発注しました。現在、発注処理は実行中です。
- **在庫**: 2個(残りわずか)
- **1日の販売数**: 8個
- **発注数**: 54個(1週間分56個から在庫2個を引いた数)
- **単価**: 2,980円
- **概算金額**: 160,920円(税抜き、54個×2,980円)
合計160,920円で3万円を超えているため承認待ちになり、RUNNING のまま止まりました。
どこで処理が止まっているのか聞いてみます。
uv run store_agent.py "米5kgの発注がまだ終わらないみたい。どこで止まってるか調べて"
発注の実行一覧を確認します。
[order___ListExecutions] {"statusFilter": "RUNNING"}
[order___DescribeExecution] {...}
[order___GetExecutionHistory] {"executionArn": "arn:aws:states:us-east-1:123456789012:execution:PurchaseOrderWorkflow:beb59a2e-..."}
発注は**承認待ち**で止まっています。エラーではなく、承認者の判断を待っている状態です。
**状況**
- 商品: 米5kg、数量54個、単価2,980円、合計160,920円
- 理由: 残りわずかのため1週間分を発注(1日販売8個×7日=56個、在庫2個を差し引いて54個)
- 現在のステップ: `WaitForApproval`(SNSで承認依頼を送信済み)
- 承認依頼メールの送信は 2026-10-30 09:54:31 (GMT) に完了しています。
**止まっている理由**
承認依頼メールに記載の承認・却下リンクから、承認者が判断するのを待っています。承認か却下の操作があるまで次に進めません。
**今後の動き**
- 承認されると発注が進みます。
- 却下されると発注は行われません。
- 24時間(86,400秒)以内に応答がないとタイムアウトします。
実行ARNを渡していなくても ListExecutions で実行中の発注を探し、GetExecutionHistory で承認待ちの状態で止まっていると特定してくれました。システムプロンプトには履歴の確認を書いていませんでしたが、承認依頼のメールを送った時刻や、承認待ちが24時間でタイムアウトすることまで履歴から読み取れていますね!
店長がメールで承認する
店長のメールには、こんな承認依頼が届きました。

発注担当のエージェントがワークフローに渡した入力から、商品・数量・合計・理由を本文に入れています。リンクを開くとBasic認証のパスワードを聞かれ、正しく入力すると承認ページが表示されます。

「承認する」を押してから、発注担当のエージェントに結果を聞いてみます。
uv run store_agent.py "さっきの米5kgの発注はどうなった?"
[order___ListExecutions] {}
[order___DescribeExecution] {...}
[order___DescribeExecution] {...}
米5kgの発注は完了しています(ステータス:SUCCEEDED)。
- **商品**:米5kg
- **発注数**:54個(単価 2,980円)
- **合計金額**:160,920円
- **発注理由**:残りわずかだった1週間分を発注(1日販売数8個×7日=56個、在庫2個を差し引いて54個)
店長の承認で止まっていたワークフローが再開して ORDERED になりました!
実行履歴を見ても、承認待ちの WaitForApproval を抜けて PlaceOrder へ進んでいます。
aws stepfunctions get-execution-history --execution-arn <米5kgの実行ARN> \
--query 'events[].[id,type,stateEnteredEventDetails.name]' --output text
4 TaskStateEntered WaitForApproval
5 TaskScheduled None
6 TaskStarted None
7 TaskSubmitted None
8 TaskSucceeded None
9 TaskStateExited None
10 ChoiceStateEntered IsApproved
11 ChoiceStateExited None
12 PassStateEntered PlaceOrder
13 PassStateExited None
14 ExecutionSucceeded None
発注の金額による分岐と承認の手順はステートマシンが担っているので、エージェントが承認を飛ばして発注を確定させることはない作りを簡単に体験できましたね!
ステートマシンを差し替えられないか確認する
最後に固定したステートマシンをエージェント側から変えられないか試します。
ツールを1つ呼び出す call_tool.py で、order___StartExecution に在庫照会のステートマシンのARNを渡してみます。
call_tool.py
import asyncio
import json
import os
import sys
from mcp import ClientSession
from mcp_proxy_for_aws.client import aws_iam_streamablehttp_client
async def main(name: str, arguments: dict):
async with aws_iam_streamablehttp_client(
endpoint=os.environ["GATEWAY_URL"],
aws_region="us-east-1",
aws_service="bedrock-agentcore",
) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool(name, arguments)
print("isError:", result.isError)
for c in result.content:
print(c.text)
asyncio.run(main(sys.argv[1], json.loads(sys.argv[2])))
uv run call_tool.py order___StartExecution "{\"input\": \"{}\", \"stateMachineArn\": \"$STOCK_ARN\"}"
isError: True
IllegalArgumentException - Invalid request parameters: cannot set parameter(s): [/stateMachineArn]
Gatewayが呼び出しを拒否しました!変なマシンとか呼び出されないようになっていますね!
Cedarで発注の取り消しを店長だけに許可する
Gatewayに付けたPolicy Engine(Cedar)でツールごとに呼び出せる人を制限してみます。
発注の取り消し(order___StopExecution)は店長のロールだけに許可し、発注担当のエージェントからは呼び出せないようにします。

ポリシーはCDKで作っています。Cedarはデフォルトで拒否なので、まずこのGatewayのツールをIAMの呼び出し元に許可し、そのうえで取り消しだけ店長以外を拒否します。
// Cedarのポリシー
const gatewayEntity = `AgentCore::Gateway::"${gateway.gatewayArn}"`;
const cedar = (id: string, statement: string, ignoreFindings = false) =>
new agentcore.Policy(this, id, {
policyEngine,
statement: agentcore.PolicyStatement.fromCedar(statement),
validationMode: ignoreFindings ? agentcore.PolicyValidationMode.IGNORE_ALL_FINDINGS : undefined,
});
// Cedarはデフォルト拒否なので、このGatewayのツールをIAMの呼び出し元に許可しておく
cedar("AllowGatewayTools", `permit(principal is AgentCore::IamEntity, action, resource == ${gatewayEntity});`, true);
// 発注の取り消しは店長のロールだけ(ポリシーはツールと突き合わせて検証されるので、ターゲットの後に作る)
cedar("OnlyManagerCanStop", `forbid(principal is AgentCore::IamEntity, action == AgentCore::Action::"order___StopExecution", resource == ${gatewayEntity})
unless { principal.id like "*sfn-gateway-tools-manager*" };`).node.addDependency(orderTarget);
Policyはツールの定義と照合されるため、ターゲットの作成後に作るよう依存関係を付けています。
ロール名の部分一致で店長かどうかを判定しています。
Gatewayの認証をJWTにした場合も同じように書けます。呼び出し元は AgentCore::OAuthUser になり、トークンのクレームを principal.getTag("<クレーム名>") で参照できます。ロール名の代わりにクレームで店長かどうかを判定する形です。
承認待ちの米の発注がある状態で、発注担当のエージェントに取り消しを頼んでみます。
uv run store_agent.py "さっきの米5kgの発注、やっぱり取り消して"
[order___ListExecutions] {"statusFilter": "RUNNING"}
[order___DescribeExecution] {...}
米5kgの発注(54袋、単価2,980円、合計160,920円)はまだ実行中で、取り消せていません。
私の持っているツールには、実行中の発注を停止する機能がありません。
ポリシーで禁止したツールは tools/list にも出てこなくなり、発注担当のエージェントからは取り消しのツール自体が見えていませんでした。call_tool.py で直接呼び出しても拒否されます。
Tool Execution Denied: Tool call not allowed due to policy enforcement [Policy evaluation denied due to SfnGatewayToolsStackOnlyManagerCanStopFD3C1D49-...]
店長のロールに切り替えると tools/list に order___StopExecution が出てきて、取り消しも成功しました。
# サブシェルで実行し、終わったら元の認証情報に戻す
(
CREDS=$(aws sts assume-role --role-arn <ManagerRoleArnの値> --role-session-name manager --query Credentials)
export AWS_ACCESS_KEY_ID=$(echo $CREDS | jq -r .AccessKeyId)
export AWS_SECRET_ACCESS_KEY=$(echo $CREDS | jq -r .SecretAccessKey)
export AWS_SESSION_TOKEN=$(echo $CREDS | jq -r .SessionToken)
uv run call_tool.py order___StopExecution '{"executionArn": "<米5kgの実行ARN>"}'
)
isError: False
{"stopDate":1.793360833268E9}
発注の実行は ABORTED になりました!承認の手順はワークフロー・誰がどのツールを使えるかはCedarと、役割を分けて管理できるのはいいですね。
入力の中身ではCedarの条件を書けなかった
発注理由(reason)のない発注を拒否するポリシーも試しましたが、こちらは動きませんでした。Step Functionsのツールの入力は input というJSONの文字列なので、context.input.input に like で文字列の条件を書いています。
forbid(principal is AgentCore::IamEntity, action == AgentCore::Action::"order___StartExecution", resource == AgentCore::Gateway::"<GatewayのARN>")
unless { context has input && context.input has input && context.input.input like "*\"reason\"*" };
ポリシーは作成できたものの、発注理由があってもなくても拒否されました。
Tool Execution Denied: Tool call not allowed due to policy enforcement [Authorization denied due to policy evaluation errors:
Value does not match expected type]
条件を減らして試すと context has input までは評価でき、context.input has input を入れた時点で同じ評価エラーになりました。ポリシーの検証では context.input は {input?: String} の形として扱われていますが、実際の評価では型が合わないようです。評価エラーのときは拒否されるので、この条件を入れると発注がすべて止まってしまいます。
現時点では、金額や理由など入力の中身による判断はワークフロー側に持たせるのが良さそうですが、もしうまくいくやり方あれば教えてください・・・!
後片付け
スタックを削除します。
cd cdk
pnpm exec cdk destroy -c email=<店長のメールアドレス>
使いどころを考えてみる
どういったところが嬉しいんでしょうか。少しだけ考えてみます。
裏側でAWSのサービスを直接呼び出せる
Step FunctionsはDynamoDBなどの最適化された統合に加え、AWS SDK統合で200以上のサービスのAPIをLambdaなしで呼び出せます。今回の在庫照会もDynamoDBの GetItem を直接呼び出しているだけなので、S3やほかのサービスのデータを取得する処理も同じようにツールにできそうです。
コードを書かずともマネージドに他のサービスと連携して、かつエージェントの処理と切り出せるのは良いかもなと感じました。
既存のワークフローと連携できる
すでに動いているワークフローをステートマシンに手を入れずにエージェントからも呼び出せるようになるのは嬉しいかなと思いました!

たとえば毎朝の発注バッチ・月末の売上集計・返品の承認フローがすでにStep Functionsで動いているものがあるとします。
これまでEventBridgeのスケジュールや業務画面から起動していたワークフローにGatewayといった入口を足すことでAIエージェントから簡単に呼び出せるようになります。
特に業務ロジックを変えたりエージェント側に実装し直したりせずに、呼び出しや判断を任せられるのは良さそうだと思いました!
ただつなぐときは次の点に注意が必要です。ドキュメントからピックアップして紹介しています。
- 1つの操作に固定できるステートマシンは1つなので複数のワークフローを
StartExecutionで公開するときはワークフローごとにターゲットを作る必要がある - ツールの説明は汎用的なので
inputに入れるJSONの形はシステムプロンプトやスキルでエージェントに伝える - Gatewayのターゲットはサービスロールを共有するので権限の境界を分けたいワークフローはGatewayごと分ける
- GatewayはStep Functionsを同じリージョンで呼び出すので別のリージョンのステートマシンはつなげない。ステートマシンと同じリージョンにGatewayを作る
- 既存のワークフローがExpressだと
ListExecutions・DescribeExecution・GetExecutionHistory・StopExecutionは使えない。すぐ結果がほしい処理はStartSyncExecutionで受け取り、状況を追わせたい処理はStandardで作る
AIエージェントに全部を任せたくない処理を切り出せる
もちろん既存のワークフローだけでなく新しく作る処理でも使えます!
発注のように手順はルールどおりに確実に実行したいけれど、実行するかどうかの判断はAIエージェントに任せたいケースがあった際は手順をワークフローに固めておけば、判断はエージェント・決まった手順はワークフローと役割を分けられるのは嬉しいポイントになるのかなと!

私のふわっとした理解では上記メリットがあるのかなと思いました!
色々とユースケースふかぼっていきたいですね!
おわりに
GatewayのターゲットにStep Functionsを追加して簡単にツール化できるようになったのはいいですね!既存のワークフローとの連携や固定化させたい手順などをAIエージェントに実行させるのに良さそうですし広がりを感じました!良きユースケースを模索していきたいですね!
本記事が少しでも参考になりましたら幸いです。最後までご覧いただきありがとうございました!








