Guide · EventBridge Scheduler · SDK Targets
EventBridge Scheduler SDK Targets for MCP Servers — ECS RunTask, Step Functions, DynamoDB
EventBridge Scheduler's universal target capability lets you invoke 270+ AWS SDK API operations directly — no Lambda intermediary required. For MCP server operators, this means you can run a Fargate task to probe a set of MCP endpoints on a schedule, start a Step Functions workflow that coordinates a multi-step health sweep, or write a scheduled status record to DynamoDB, all without writing any Lambda code. The target ARN follows the pattern arn:aws:scheduler:::aws-sdk:<service>:<action> — for example, arn:aws:scheduler:::aws-sdk:ecs:runTask — and the target input is the JSON body of the SDK API call. The execution role needs permissions on both the SDK action and on SQS SendMessage for the DLQ. The input transformer uses EventBridge context variables (<$.context.scheduledTime>, <$.context.scheduleArn>) to inject schedule metadata into the API call body, enabling idempotent writes without a Lambda function to do the enrichment.
TL;DR
To run a Fargate task on a schedule: set Arn: "arn:aws:scheduler:::aws-sdk:ecs:runTask" and set Input to the JSON of the ECS RunTask API call (cluster, taskDefinition, networkConfiguration, overrides). The execution role needs ecs:RunTask on the task definition and iam:PassRole on the ECS task execution role. Use the input transformer's <$.context.scheduledTime> variable to stamp the scheduledTime into a container environment variable for idempotency. See the core scheduler guide for execution role trust policy and retry/DLQ setup.
SDK target architecture
When you set a schedule target to an SDK integration ARN (arn:aws:scheduler:::aws-sdk:service:action), Scheduler calls the AWS SDK on your behalf using the execution role credentials. The Input field contains the request body as a JSON string — it must be valid JSON matching the SDK API's request structure. Scheduler does not validate the input against the SDK schema at schedule-creation time; malformed input only surfaces as an invocation failure at runtime.
# SDK target ARN format
arn:aws:scheduler:::aws-sdk:<service>:<camelCaseAction>
# Examples of SDK target ARNs relevant to MCP monitoring:
arn:aws:scheduler:::aws-sdk:ecs:runTask
arn:aws:scheduler:::aws-sdk:states:startExecution
arn:aws:scheduler:::aws-sdk:sqs:sendMessage
arn:aws:scheduler:::aws-sdk:dynamodb:putItem
arn:aws:scheduler:::aws-sdk:sns:publish
arn:aws:scheduler:::aws-sdk:codepipeline:startPipelineExecution
arn:aws:scheduler:::aws-sdk:lambda:invokeFunction # same as using Lambda target directly
arn:aws:scheduler:::aws-sdk:cloudwatch:putMetricData
# Note: action name is camelCase matching the SDK method name
# "RunTask" → "runTask", "StartExecution" → "startExecution"
# Use the SDK API reference to find the exact camelCase action name
Not all AWS SDK actions are supported — the supported list covers common orchestration and messaging actions. If your required action is not available as an SDK target, use a Lambda function as the intermediary (see the Lambda target guide). You can verify whether a specific action is supported by attempting to create a schedule with that ARN; Scheduler returns a ValidationException if the service/action combination is not on the supported list.
ECS RunTask target
Running an ECS Fargate task on a schedule is the most powerful SDK target pattern for MCP monitoring. Instead of a long-running probe daemon, you run a short-lived task that probes your MCP endpoints, writes results, and exits. The task runs for 30–60 seconds, incurs minimal cost, and does not hold open connections between checks.
# Create a schedule that runs a Fargate probe task every 15 minutes
aws scheduler create-schedule \
--name mcp-probe-fargate-15m \
--group-name mcp-monitoring \
--schedule-expression "rate(15 minutes)" \
--flexible-time-window '{"Mode":"FLEXIBLE","MaximumWindowInMinutes":2}' \
--target '{
"Arn": "arn:aws:scheduler:::aws-sdk:ecs:runTask",
"RoleArn": "arn:aws:iam::123456789012:role/SchedulerExecutionRole",
"Input": "{
\"cluster\": \"mcp-monitoring-cluster\",
\"taskDefinition\": \"mcp-probe-task:12\",
\"launchType\": \"FARGATE\",
\"networkConfiguration\": {
\"awsvpcConfiguration\": {
\"subnets\": [\"subnet-abc123\", \"subnet-def456\"],
\"securityGroups\": [\"sg-mcp-probe\"],
\"assignPublicIp\": \"DISABLED\"
}
},
\"overrides\": {
\"containerOverrides\": [{
\"name\": \"probe\",
\"environment\": [
{\"name\": \"PROBE_TARGET\", \"value\": \"https://api.example.com/mcp\"},
{\"name\": \"SCHEDULE_TIME\", \"value\": \"<$.context.scheduledTime>\"}
]
}]
}
}",
"RetryPolicy": {
"MaximumRetryAttempts": 0,
"MaximumEventAgeInSeconds": 300
},
"DeadLetterConfig": {
"Arn": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
}'
The input JSON is a string (it must be JSON-encoded within the outer JSON). The <$.context.scheduledTime> placeholder is substituted by Scheduler at invocation time with the ISO 8601 UTC timestamp of when the schedule was supposed to fire (not when it actually fired — the flexible window shifts actual fire time, but scheduledTime reflects the nominal schedule time). This is the key for idempotency: use scheduledTime as a deduplication key in your probe task to ensure a re-run from a retry does not produce a duplicate probe record.
The execution role for ECS RunTask requires three permissions:
# Execution role policy for ECS RunTask SDK target
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ECSRunTask",
"Effect": "Allow",
"Action": "ecs:RunTask",
"Resource": "arn:aws:ecs:us-east-1:123456789012:task-definition/mcp-probe-task:*"
},
{
"Sid": "PassECSRole",
"Effect": "Allow",
"Action": "iam:PassRole",
"Resource": [
"arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"arn:aws:iam::123456789012:role/mcp-probe-task-role"
],
"Condition": {
"StringEquals": {
"iam:PassedToService": "ecs-tasks.amazonaws.com"
}
}
},
{
"Sid": "DLQ",
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
]
}
The iam:PassRole permission on both the task execution role and the task role is required — without it, RunTask fails with AccessDenied: User is not authorized to perform iam:PassRole. This error message names the role that could not be passed, which makes it straightforward to diagnose: add the specific role ARN to the PassRole statement.
Set MaximumRetryAttempts: 0 for Fargate task targets. Retrying a RunTask starts a second task — if the first task is still running (because it was slow, not because it failed), you now have two probe tasks executing simultaneously, potentially double-writing results. Let the next scheduled invocation handle the retry instead.
Step Functions StartExecution target
For MCP monitoring workflows that require multiple coordinated steps — probe, analyze, alert, acknowledge — use Step Functions as the Scheduler target. This keeps orchestration logic in the state machine definition rather than in a monolithic Lambda function.
# Schedule a Step Functions execution every hour
aws scheduler create-schedule \
--name mcp-weekly-report-1h \
--group-name mcp-monitoring \
--schedule-expression "cron(0 * * * ? *)" \
--flexible-time-window '{"Mode":"OFF"}' \
--target '{
"Arn": "arn:aws:scheduler:::aws-sdk:states:startExecution",
"RoleArn": "arn:aws:iam::123456789012:role/SchedulerExecutionRole",
"Input": "{
\"stateMachineArn\": \"arn:aws:states:us-east-1:123456789012:stateMachine:mcp-uptime-report\",
\"name\": \"hourly-report-<$.context.scheduledTime>\",
\"input\": \"{\\\"reportType\\\":\\\"hourly\\\",\\\"scheduledTime\\\":\\\"<$.context.scheduledTime>\\\"}\"
}",
"RetryPolicy": {
"MaximumRetryAttempts": 1,
"MaximumEventAgeInSeconds": 600
},
"DeadLetterConfig": {
"Arn": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
}'
The name field in the StartExecution input is the execution name — it must be unique within the state machine over 90 days. Using <$.context.scheduledTime> in the name gives you a unique, deterministic execution name per schedule slot. If Scheduler retries the same slot, the StartExecution call returns ExecutionAlreadyExists — which is a non-fatal error for idempotency purposes, but Scheduler treats it as a failure and may retry. To avoid this, use Step Functions Express Workflows (which allow non-unique execution names and have a 5-minute maximum duration) for high-frequency scheduling. Standard Workflows with unique names are correct for hourly or less frequent runs.
# Execution role policy for Step Functions target
{
"Statement": [
{
"Effect": "Allow",
"Action": "states:StartExecution",
"Resource": "arn:aws:states:us-east-1:123456789012:stateMachine:mcp-uptime-report"
},
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
]
}
SQS SendMessage target
Targeting SQS is useful for decoupled scheduled work: the schedule writes a message, and a consumer Lambda or Fargate service processes it asynchronously. This pattern adds resilience — if the consumer is temporarily unavailable, the message waits in the queue.
# Schedule that sends an SQS message every 10 minutes
aws scheduler create-schedule \
--name mcp-probe-queue-10m \
--group-name mcp-monitoring \
--schedule-expression "rate(10 minutes)" \
--flexible-time-window '{"Mode":"FLEXIBLE","MaximumWindowInMinutes":2}' \
--target '{
"Arn": "arn:aws:scheduler:::aws-sdk:sqs:sendMessage",
"RoleArn": "arn:aws:iam::123456789012:role/SchedulerExecutionRole",
"Input": "{
\"QueueUrl\": \"https://sqs.us-east-1.amazonaws.com/123456789012/mcp-probe-queue\",
\"MessageBody\": \"{\\\"action\\\":\\\"probe\\\",\\\"scheduledTime\\\":\\\"<$.context.scheduledTime>\\\"}\",
\"MessageGroupId\": \"mcp-probes\",
\"MessageDeduplicationId\": \"<$.context.scheduledTime>\"
}",
"RetryPolicy": {
"MaximumRetryAttempts": 2,
"MaximumEventAgeInSeconds": 300
},
"DeadLetterConfig": {
"Arn": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
}'
For FIFO queues (.fifo suffix), MessageGroupId and MessageDeduplicationId are required in the Input. Use <$.context.scheduledTime> as the MessageDeduplicationId — SQS FIFO deduplicates within a 5-minute window, so Scheduler retries for the same scheduled slot will be deduplicated as long as they fall within that window. Note: the SQS DLQ configured on the Scheduler schedule is different from the SQS queue's own DLQ — the Scheduler DLQ captures failures to even enqueue (e.g., SQS service unavailable); the queue's own DLQ captures messages that cannot be processed by the consumer.
# Execution role policy for SQS target
{
"Statement": [
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:123456789012:mcp-probe-queue.fifo"
},
{
"Effect": "Allow",
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:123456789012:scheduler-dlq"
}
]
}
DynamoDB PutItem target
The DynamoDB PutItem target is useful for writing scheduled status heartbeats — a record that proves the schedule ran at the expected time. Downstream dashboards can detect gaps in the heartbeat record to identify missed schedule invocations.
# Schedule that writes a heartbeat to DynamoDB every minute
aws scheduler create-schedule \
--name mcp-scheduler-heartbeat-1m \
--group-name mcp-monitoring \
--schedule-expression "rate(1 minute)" \
--flexible-time-window '{"Mode":"OFF"}' \
--target '{
"Arn": "arn:aws:scheduler:::aws-sdk:dynamodb:putItem",
"RoleArn": "arn:aws:iam::123456789012:role/SchedulerExecutionRole",
"Input": "{
\"TableName\": \"mcp-scheduler-heartbeat\",
\"Item\": {
\"pk\": {\"S\": \"heartbeat#mcp-probe\"},
\"sk\": {\"S\": \"<$.context.scheduledTime>\"},
\"scheduleArn\": {\"S\": \"<$.context.scheduleArn>\"},
\"attempt\": {\"N\": \"<$.context.attemptNumber>\"},
\"ttl\": {\"N\": \"1757980800\"}
},
\"ConditionExpression\": \"attribute_not_exists(sk)\"
}"
}'
# Available context variables for input transformer:
# <$.context.scheduledTime> — ISO 8601 UTC nominal schedule time
# <$.context.scheduleArn> — full ARN of the schedule
# <$.context.scheduleName> — name of the schedule (without group prefix)
# <$.context.scheduleGroupName> — name of the schedule group
# <$.context.attemptNumber> — 0 for first attempt, increments on retry
The ConditionExpression: attribute_not_exists(sk) ensures idempotency — if Scheduler retries the same scheduled slot (same scheduledTime), the PutItem fails with ConditionalCheckFailedException. Scheduler treats this as a target failure and retries per the retry policy. Set MaximumRetryAttempts: 0 if you are using conditional writes for idempotency — you do not want Scheduler to retry a ConditionalCheckFailed as if it were a transient error.
The DynamoDB PutItem input must use DynamoDB's typed JSON format — each value must be an object with a type key (S for string, N for number, BOOL for boolean). Plain JSON values are not accepted by the DynamoDB API. The number type is still a string in the JSON: {"N": "42"} not {"N": 42}.
Input transformer context variables
All SDK targets support the same set of context variables in the Input string. These are substituted by Scheduler before sending the request to the target API:
| Variable | Value | Type | Use case |
|---|---|---|---|
<$.context.scheduledTime> | ISO 8601 UTC nominal schedule time | String | Idempotency key, timestamp for records, deduplication |
<$.context.scheduleArn> | Full ARN of the schedule | String | Audit trail, multi-schedule disambiguition |
<$.context.scheduleName> | Schedule name (no group prefix) | String | Logging, tagging ECS tasks |
<$.context.scheduleGroupName> | Schedule group name | String | Routing logic in downstream consumers |
<$.context.attemptNumber> | 0-based retry count (0 = first attempt) | String (numeric) | Distinguish retries from first attempts in logs; skip DLQ logic on attempt 0 |
Variables are substituted as strings — even attemptNumber and numeric IDs are injected as JSON string values. If your target API expects a number, wrap the substitution in a numeric conversion in the downstream handler. For DynamoDB, use the N type key: {"N": "<$.context.attemptNumber>"} — DynamoDB accepts the value as a number even though it is encoded as a string in the typed JSON format.
Failure modes reference
| Failure | Symptom | Fix |
|---|---|---|
| SDK action not in supported list | ValidationException on schedule creation | Not all 270+ SDK actions are available as Scheduler targets; check the EventBridge Scheduler supported targets list in AWS docs; use Lambda as intermediary for unsupported actions |
| ECS RunTask missing iam:PassRole | RunTask fails: AccessDenied: iam:PassRole on ecsTaskExecutionRole | Add iam:PassRole on both the task execution role and the task IAM role to the Scheduler execution role |
| Step Functions ExecutionAlreadyExists | Scheduler treats it as failure and retries → duplicate execution attempts | Use <$.context.scheduledTime> in execution name for uniqueness; use Express Workflows for high-frequency (they allow non-unique names); or catch ExecutionAlreadyExists in error handling |
| DynamoDB typed JSON format wrong | RunTask fails: SerializationException | DynamoDB PutItem requires typed JSON ({"S":"value"} not "value"); all values must be wrapped in type objects |
| Context variable in wrong format | Input treated as literal string; variable not substituted | Syntax is <$.context.variableName> with angle brackets and dollar-dot prefix; variables are case-sensitive; check AWS docs for exact names |
| SQS FIFO missing MessageGroupId | InvalidParameterValue: MessageGroupId parameter is required | FIFO queues require MessageGroupId and MessageDeduplicationId in the Input; standard queues do not — ensure you are matching the queue type |