Example: AWS Step Functions (Public Preview)
Overview
This guide walks through setting up a custom ETL connector for AWS Step Functions using a pre-built example implementation.
Step Functions is AWS's serverless workflow orchestrator. This example connector talks to the Step Functions API with boto3 to collect state-machine metadata and execution history, so Monte Carlo can track runs, alert on failures, and surface a performance dashboard. The agent framework handles scheduling, pagination, and delivery β you only supply credentials.
This implementation is available as a reference in the mcd-public-resources repository. You can use it as-is or as a starting point for your own customizations.
One account + region per connection
The connector collects every state machine the supplied credentials can reach in a single AWS account and region. To monitor multiple regions or accounts, add one integration per account/region.
Feature support
| Category | Capability | Support |
|---|---|---|
| Step Functions | Execution Failure Alerts | β |
| Step Functions | Webhook triggered collection | β |
| Step Functions | Run History | β |
| Lineage | Table-to-Table Lineage | β |
| Lineage | Step Function-to-Step Function Lineage | β |
| Lineage | Column-level Lineage | β |
Table-to-table lineage is produced via SQL query tagging, not declared
inputs/outputsβ the Step Functions API exposes which services a state invokes, but not the warehouse tables read or written. See Lineage via SQL query tagging below. Job-to-job lineage (one state machine starting another) is derived automatically from the definition.
Prerequisites
- A running custom-connector-setup repository β see Custom ETL Connectors for initial setup.
- AWS credentials that can reach the Step Functions API in the target account and region (see Configure credentials for the options).
- An IAM policy granting the read-only Step Functions actions listed below.
Step Functionsβspecific notes
Understanding how Step Functions' concepts map to Monte Carlo's ETL data model is helpful if you plan to customize the connector:
| Monte Carlo concept | Step Functions concept |
|---|---|
| Connection | An AWS account + region β all state machines the credentials can reach. |
| Group | Not used β Step Functions has no notion of the same job in multiple named environments.;; |
| Job | A state machine, keyed by its stateMachineArn. |
| Task | A state in the state machine's definition, emitted at runtime as task_runs. |
| Run | An execution, keyed by its executionArn. |
- Jobs are discovered by listing state machines β
fetch_metadatalists every state machine in the account/region and parses each one's Amazon States Language (ASL) definition into tasks, recursing into Parallel branches and Map processors so nested states appear too. Task edges (Next/Choices/Catch) become the job's DAG. - Job-to-job lineage β a state that starts another state machine via the
states:startExecutionintegration records the child's ARN as a triggered job, so Monte Carlo links parent β child state machines automatically. - Runs of any trigger are reported β polling lists all executions in the collection window regardless of what started them. (Step Functions doesn't expose how an execution was triggered, so no trigger type is reported.)
- No schedule β state machines are triggered externally (e.g. EventBridge Scheduler), so the connector emits no schedule.
- EXPRESS workflows are skipped β
ListExecutionsdoesn't support EXPRESS state machines (their history lives in CloudWatch Logs). STANDARD workflows are fully supported. - Distributed Map β child iterations run as separate executions with their own ARNs, so per-item detail isn't nested under the parent run's
task_runs. Inline Map/Parallel iterations are captured.
Setup
1. Scaffold the connector
Use the create_connector script with the --etl flag to scaffold the connector directory. This generates a unique identifier for your connector in manifest.json β this ID is how Monte Carlo distinguishes connector types, so it must be generated per-installation.
python scripts/create_connector.py aws_step_functions --etlYou will be asked what the Step Functions name for "Group", "Job" and "Task" are. This will change how these assets are named in Monte Carlo. Step Functions has no group concept, so leave that prompt blank. We recommend:
What does this tool call a group of jobs? (optional, blank to skip):
What does this tool call a job? (default: Job): State Machine
What does this tool call a task? (default: Task): State
Icon URL (leave blank to skip): The script also prompts for an optional icon URL to display the Step Functions logo alongside the connection in the Monte Carlo UI.
You will now have a manifest.json file created. Replace run_status_mapping and credentials_schema with the following:
"run_status_mapping": {
"SUCCEEDED": "success",
"FAILED": "failed",
"RUNNING": "in_progress",
"TIMED_OUT": "timed_out",
"ABORTED": "cancelled",
"PENDING_REDRIVE": "queued"
},
"credentials_schema": {
"connect_args": {
"type": "dict",
"required": true,
"schema": {
"region_name": { "type": "string", "required": true, "empty": false },
"role_arn": { "type": "string", "required": false },
"external_id": { "type": "string", "required": false },
"role_session_name": { "type": "string", "required": false },
"aws_access_key_id": { "type": "string", "required": false },
"aws_secret_access_key": { "type": "string", "required": false },
"aws_session_token": { "type": "string", "required": false },
"endpoint_url": { "type": "string", "required": false }
}
}
}The full manifest file should look something like this with a unique connection_type:
{
"connection_type": "custom-etl-connector-XXXXXXX",
"connection_name": "aws_step_functions",
"asset_class": "etl",
"terminology": {
"job": "State Machine",
"task": "State"
},
"run_status_mapping": {
"SUCCEEDED": "success",
"FAILED": "failed",
"RUNNING": "in_progress",
"TIMED_OUT": "timed_out",
"ABORTED": "cancelled",
"PENDING_REDRIVE": "queued"
},
"credentials_schema": {
"connect_args": {
"type": "dict",
"required": true,
"schema": {
"region_name": { "type": "string", "required": true, "empty": false },
"role_arn": { "type": "string", "required": false },
"external_id": { "type": "string", "required": false },
"role_session_name": { "type": "string", "required": false },
"aws_access_key_id": { "type": "string", "required": false },
"aws_secret_access_key": { "type": "string", "required": false },
"aws_session_token": { "type": "string", "required": false },
"endpoint_url": { "type": "string", "required": false }
}
}
}
}2. Replace with the example implementation
Clone or download the example from mcd-public-resources and replace the stub connector.py with the pre-built implementation:
cp <path-to-mcd-public-resources>/custom_connectors/aws_step_functions/connector.py etl_connectors/aws_step_functions/The connector uses boto3, so add it to the connector's requirements.txt:
boto3>=1.34.0No Dockerfile.extra is needed β boto3 is a pure-Python package with no system dependencies.
3. Configure credentials
Step Functions is an AWS service, so the connector authenticates with standard AWS credentials. region_name is always required; how it authenticates depends on where the agent runs. Two things are independent here:
- Where the agent runs β any cloud or on-prem. Flexible.
- What it talks to β the Step Functions API, which is always AWS.
So the connector always needs AWS credentials; the only deployment-dependent choice is how they're supplied. You provide them through self-hosted credentials β the values you put in connect_args become the connection's credentials. Create etl_connectors/aws_step_functions/credentials.json using one of the models below.
The generic agent runs anywhere (any Kubernetes cluster, Docker, on-prem, or a non-AWS cloud), so it usually has no ambient AWS identity. In that case you must supply explicit credentials β a static key (Option A) or an assumed role (Option B). Option C (no keys, relying on the pod/instance role) works only when the agent is deployed in AWS.
Option A β Static IAM user access key (works from any agent host). A long-lived access key (AKIAβ¦) works everywhere, including local testing. aws_session_token is only needed for temporary (ASIAβ¦) credentials, which expire:
{
"connect_args": {
"region_name": "us-east-1",
"aws_access_key_id": "AKIA...",
"aws_secret_access_key": "..."
}
}Option B β Assume an IAM role (auto-refreshing; works from any agent host). The connector calls sts:AssumeRole and refreshes the temporary credentials automatically, so nothing expires. This avoids a long-lived key with broad access β you hand out only a scoped, assumable role. The base identity used to assume the role comes from the keys in Option A, or from the pod/instance role when the agent runs in AWS:
{
"connect_args": {
"region_name": "us-east-1",
"aws_access_key_id": "AKIA...",
"aws_secret_access_key": "...",
"role_arn": "arn:aws:iam::<account-id>:role/mc-stepfunctions-readonly",
"external_id": "<external-id-if-required>"
}
}Option C β Pod / instance role (AWS-hosted agents only). When the agent runs in AWS β EKS with IRSA / EKS Pod Identity, EC2 with an instance profile, or ECS with a task role β boto3's default credential chain resolves the attached role automatically, so no secrets are needed:
{
"connect_args": {
"region_name": "us-east-1"
}
}For Monte Carlo's Kubernetes agent on EKS, the service account is bound to an IAM role via IRSA/Pod Identity β but that role is provisioned for the agent's own needs and does not include Step Functions permissions by default. To use Option C you must attach the read-only policy below to that role. If you'd rather not modify the agent's role, use Option A or B instead.
| Field | Description | Default |
|---|---|---|
| region_name | AWS region of the Step Functions service (required) | |
| role_arn | IAM role to assume (auto-refreshing STS credentials) | |
| external_id | External ID required by the assumed role's trust policy | |
| role_session_name | STS session name for the role assumption | monte-carlo-etl |
| aws_access_key_id | Static access key β omit to use the default chain | |
| aws_secret_access_key | Static secret key | |
| aws_session_token | Session token for temporary base credentials | |
| endpoint_url | Override the service endpoint (testing/localstack) |
IAM permissions
The connector makes only these read-only calls. Attach this policy to the identity (or role) it uses:
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "MonteCarloStepFunctionsRead",
"Effect": "Allow",
"Action": [
"states:ListStateMachines",
"states:DescribeStateMachine",
"states:ListExecutions",
"states:DescribeExecution",
"states:GetExecutionHistory"
],
"Resource": "*"
}]
}ListStateMachines is account-scoped and needs Resource: "*"; the others can be narrowed to specific ARNs. The AWS managed policy AWSStepFunctionsReadOnlyAccess also covers all of these.
Assume-role trust policy (Option B). The target role's trust policy must allow the base identity to assume it, and the base identity needs
sts:AssumeRoleon the role:{ "Version": "2012-10-17", "Statement": [{ "Effect": "Allow", "Principal": { "AWS": "arn:aws:iam::<base-account>:role/<agent-execution-role>" }, "Action": "sts:AssumeRole", "Condition": { "StringEquals": { "sts:ExternalId": "<external-id>" } } }] }Drop the
Conditionblock if you don't setexternal_id.
4. Build and run the test suite
Build the test Docker image, then run each test section to validate the connector against the Step Functions API:
docker compose build
# Verify connection (builds the client and lists state machines)
CONNECTOR=aws_step_functions docker compose run --rm test -m etl_connection
# Metadata collection (state machines + parsed task DAG)
CONNECTOR=aws_step_functions docker compose run --rm test -m etl_metadata
# Run details (polling + webhook modes)
CONNECTOR=aws_step_functions docker compose run --rm test -m etl_run_detailsThe test suite validates that the connector returns correctly structured dicts matching the EtlAsset and EtlRunEvent schemas. The collection window defaults to 7 days (configurable via the ETL_TEST_WINDOW_HOURS env var) β make sure the account has recent executions so the run-detail tests have data.
5. Export and build the agent image
Once all tests pass, build the deployable image:
python scripts/generate_agent_image.py aws_step_functions6. Deploy, register, and connect
From here, follow the standard custom ETL connector workflow:
- Push the image to your container registry and deploy the agent, providing your production credentials via self-hosted credentials. If the agent is deployed in AWS, you can use the pod/instance-role model (Option C) otherwise supply a static key or assumed role (Options A/B).
- Register the connector by running agent validations in the Monte Carlo UI (Settings β Deployments β your agent β Validate).
- Add the connection through the Monte Carlo UI (Settings β Integrations β Add β Orchestration β Custom ETL integration). Select Collect to have Monte Carlo pull on a schedule, and configure the webhook for near-real-time failure detection.
After setup, Monte Carlo collects state-machine metadata on the agent's schedule and polls run details every 60 minutes.
Near-real-time failure alerts (webhook)
Polling already catches failures with no extra setup β the agent collects run details every 60 minutes, so a failure surfaces within ~60 minutes. For near-real-time alerting, Monte Carlo exposes a webhook you POST to when an execution fails.
Step Functions publishes execution state changes to Amazon EventBridge automatically, and EventBridge can call the Monte Carlo webhook directly through an API Destination β no forwarding proxy required, because the Connection sets the required headers for you:
| Header | Value |
|---|---|
| x-mcd-id | Integration key ID |
| x-mcd-token | Integration key secret |
You get both values when you add the Custom ETL connection in Monte Carlo (step 6) β after choosing Collect, Monte Carlo lands you on a page with the webhook URL and a button to generate the integration key (its ID and secret).
flowchart LR
SF["Step Functions<br>execution fails"] --> EB["EventBridge rule<br>(FAILED / TIMED_OUT / ABORTED)"]
EB --> AD["API Destination<br>Connection adds x-mcd-id / x-mcd-token"]
AD --> MC["Monte Carlo webhook"]
MC --> A["Run collection<br>(failure alert)"]
Set it up with four resources. Fill in <INTEGRATION_KEY_ID>, <INTEGRATION_TOKEN>, and <MC_WEBHOOK_URL> from step 6.
1. Connection β holds the auth headers (stored encrypted):
aws events create-connection \
--name monte-carlo-webhook \
--authorization-type API_KEY \
--auth-parameters '{
"ApiKeyAuthParameters": { "ApiKeyName": "x-mcd-token", "ApiKeyValue": "<INTEGRATION_TOKEN>" },
"InvocationHttpParameters": {
"HeaderParameters": [
{ "Key": "x-mcd-id", "Value": "<INTEGRATION_KEY_ID>", "IsValueSecret": true }
]
}
}'2. API Destination β the Monte Carlo webhook endpoint:
aws events create-api-destination \
--name monte-carlo-webhook \
--connection-arn <CONNECTION_ARN> \
--invocation-endpoint "<MC_WEBHOOK_URL>" \
--http-method POST3. Rule β match failed executions (add stateMachineArn under detail to scope to specific jobs):
aws events put-rule \
--name step-functions-failures \
--event-pattern '{
"source": ["aws.states"],
"detail-type": ["Step Functions Execution Status Change"],
"detail": { "status": ["FAILED", "TIMED_OUT", "ABORTED"] }
}'4. Target β pass the failed execution's ARN as the job_run_id query parameter so Monte Carlo collects just that run:
aws events put-targets \
--rule step-functions-failures \
--targets '[{
"Id": "mc-webhook",
"Arn": "<API_DESTINATION_ARN>",
"RoleArn": "<EVENTBRIDGE_INVOKE_ROLE_ARN>",
"HttpParameters": {
"QueryStringParameters": { "job_run_id": "$.detail.executionArn" }
}
}]'$.detail.executionArn is the failed execution's run_source_id; the RoleArn is an IAM role EventBridge assumes to call the destination (it needs events:InvokeApiDestination on the destination ARN).
You can also forward to the bare webhook URL (omit the query parameter) β Monte Carlo then collects all runs from the last hour and picks up the failed one. Scoping with
job_run_idjust makes collection precise.
Lineage via SQL query tagging
Table β state-machine lineage is not available from the Step Functions API β states invoke services (Lambda, Glue, ECS, β¦), not warehouse tables β so the connector intentionally omits inputs/outputs. To get warehouse lineage, tag the SQL that runs inside your states with a JSON comment carrying mcd_job_id set to the connector's job_source_id (the stateMachineArn):
-- {"mcd_job_id": "arn:aws:states:us-east-1:123456789012:stateMachine:MyJob"}
CREATE TABLE ... AS SELECT ...;Optionally add mcd_task_id (a state name, i.e. a task's task_source_id) to attribute lineage to a specific state, or mcd_resource_id (the ETL connection's resource UUID) to disambiguate when multiple connections share an ARN. Monte Carlo ingests these tags through its standard warehouse query-log collection and resolves them back to the jobs and tasks this connector reports. See Lineage Between Jobs and Tables for details.
Injecting the tag from the state machine definition
You don't have to hardcode the ARN. The Step Functions Context Object exposes the state machine ARN as $$.StateMachine.Id (the same value the connector uses as job_source_id) and the current state name as $$.State.Name.
Direct SQL integrations (Athena, Redshift Data API) β the query text is a Parameters field, so prepend the tag comment right in the definition with intrinsic functions:
{
"Type": "Task",
"Resource": "arn:aws:states:::athena:startQueryExecution.sync",
"Parameters": {
"QueryString.$": "States.Format('-- \\{\"mcd_job_id\":\"{}\",\"mcd_task_id\":\"{}\"\\}\n{}', $$.StateMachine.Id, $$.State.Name, $.sql)",
"WorkGroup": "primary",
"ResultConfiguration": { "OutputLocation": "s3://.../results/" }
}
}The \\{ / \\} escapes are required because States.Format uses {} as its own placeholders; $.sql is your actual query.
Lambda / Glue / ECS that run SQL internally β the definition can't reach the SQL, so pass the ids in as input and apply them in code:
{
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "run-warehouse-sql",
"Payload": {
"mcd_job_id.$": "$$.StateMachine.Id",
"mcd_task_id.$": "$$.State.Name",
"sql.$": "$.sql"
}
}
}# in the Lambda (Snowflake example)
cur.execute("ALTER SESSION SET QUERY_TAG = %s", (json.dumps({
"mcd_job_id": event["mcd_job_id"],
"mcd_task_id": event["mcd_task_id"],
}),))ARN must match.
$$.StateMachine.Idis the base state-machine ARN, which matches thejob_source_idthe connector reports for unversioned machines. If you invoke via a version or alias ARN, the execution'sstateMachineArncan differ from the base ARN, and lineage won't line up.
Ingest the warehouse's query logs
SQL query tagging only works if Monte Carlo is collecting query logs from the warehouse the states run against (Snowflake, BigQuery, etc.). That warehouse must be connected to Monte Carlo through its own separate integration with query log collection enabled β this is what makes the tables appear in the catalog and what lets Monte Carlo read the tags and resolve the state-machine β table edges. Without it, the tags are never ingested and no lineage is produced.
Updated about 15 hours ago
