Skip to content

Conversation

@r-heimann
Copy link
Contributor

@r-heimann r-heimann commented Oct 15, 2025

Issue #, if available:

Fixes #4074

Description of changes:

This PR implements a check for

https://states-language.net/spec.html#toplevelfields

A State Machine MUST have a string field named "StartAt", whose value MUST match exactly the name of one of the "States" fields. The interpreter starts executing the machine at the named state.

Since i am not a professional Python programmer i used the help of a LLM to implement the check and the unit tests, while trying to keep it as simple as possible.
The cfn-lint "Error" messages are almost identical to the State Machine CloudFormation API, but i made it a little bit more human friendly to read.

I tested the following CloudFormation templates, which all displayed the error message in the correct State:

StartAt JSONata / JSONPath
  StartAtJSONata:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONata
        StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /StartAt
        States:
          Pass: # MISSING_TRANSITION_TARGET: State "Pass" is not reachable. at /States/Pass (not yet implemented)
            Type: Pass
            Next: Success
          Success:
            Type: Succeed

  StartAtJSONPath:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONPath
        StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /StartAt
        States:
          Pass: # MISSING_TRANSITION_TARGET: State "Pass" is not reachable. at /States/Pass (not yet implemented)
            Type: Pass
            Next: Success
          Success:
            Type: Succeed
Parallel StartAt JSONata / JSONPath
  ParallelStartAtJSONata:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONata
        StartAt: ParallelState
        States:
          ParallelState:
            Type: Parallel
            Branches:
              - StartAt: BranchState1
                States:
                  BranchState1:
                    Type: Pass
                    End: true
              - StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /States/ParallelState/Branches[1]/StartAt
                States:
                  BranchState2: # MISSING_TRANSITION_TARGET: State "BranchState2" is not reachable. at /States/ParallelState/Branches[1]/States/BranchState2 (not yet implemented)
                    Type: Pass
                    End: true
            End: true

  ParallelStartAtJSONPath:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONPath
        StartAt: ParallelState
        States:
          ParallelState:
            Type: Parallel
            Branches:
              - StartAt: BranchState1
                States:
                  BranchState1:
                    Type: Pass
                    End: true
              - StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /States/ParallelState/Branches[1]/StartAt
                States:
                  BranchState2: # MISSING_TRANSITION_TARGET: State "BranchState2" is not reachable. at /States/ParallelState/Branches[1]/States/BranchState2 (not yet implemented)
                    Type: Pass
                    End: true
            End: true
MapStartAt JSONata / JSONPath
  MapStartAtJSONata:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONata
        StartAt: MapState
        States:
          MapState:
            Type: Map
            ItemProcessor:
              ProcessorConfig:
                Mode: INLINE
              StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /States/MapStateItemProcessor/ItemProcessor/StartAt
              States:
                ItemProcessStart: # MISSING_TRANSITION_TARGET: State "ItemProcessStart" is not reachable. at /States/MapStateItemProcessor/ItemProcessor/States/ItemProcessStart (not yet implemented)
                  Type: Pass
                  End: true
            End: true


  MapStartAtJSONPath:
    Type: AWS::StepFunctions::StateMachine
    Properties:
      RoleArn: !GetAtt StepFunctionRole.Arn
      Definition:
        QueryLanguage: JSONPath
        StartAt: MapState
        States:
          MapState:
            Type: Map
            ItemProcessor:
              ProcessorConfig:
                Mode: INLINE
              StartAt: FAIL # MISSING_TRANSITION_TARGET: Missing 'Next' target: FAIL at /States/MapStateItemProcessor/ItemProcessor/StartAt
              States:
                ItemProcessStart: # MISSING_TRANSITION_TARGET: State "ItemProcessStart" is not reachable. at /States/MapStateItemProcessor/ItemProcessor/States/ItemProcessStart (not yet implemented)
                  Type: Pass
                  End: true
            End: true

The PR also includes Unit Tests for

  • StartAt
  • Map StartAt
  • Parallel StartAt

which were succesful in local testing.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Includes UnitTest for

- StartAt
- Map StartAt
- Parallel StartAt
@r-heimann
Copy link
Contributor Author

@kddejong The line length limit is mandatory for cfn-lint code, right?

[tool.ruff]
# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default.
lint.select = ["E", "F"]
lint.ignore = []
line-length = 88

Copy link
Contributor

@kddejong kddejong left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few checks to validate the types are we hope they are. Happy path says these are the right types but since we are linting we have to assume they could be wrong.

states = definition.get("States")

# Check if StartAt is missing
if start_at is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if start_at is None:
if not isinstance(start_at, str) and not isinstance(states, dict):

state_type = state.get("Type")

if state_type == "Parallel":
branches = state.get("Branches", [])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
branches = state.get("Branches", [])
branches = state.get("Branches", [])
if not isinstance(branches, list):
continue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: AWS::StepFunctions::StateMachine - Recognize if StartAt is unreachable

2 participants