I managed to solve this myself by including an extra step.
So my StepFunction loops around back on itself, so the first time it's executed the value doesn't exist and I need to pass in 0, then increment it as it loops around.
"Prepare": {
"Type": "Task",
"TimeoutSeconds": 60,
"Resource": "...",
"ResultPath": "$.prepareResult",
"Parameters": {
"Runs.$": "$.prepareResult.runs"
},
...
"Next": "PrepareDecision"
},
The first time this is run prepareResult does not exist, so this throws an error when executed.
Once executed it returns a result which is appended to prepareResult and contains the value for runs.
To default the value I included a PrepareDefaults step, which is executed before this step.
"PrepareDefaults": {
"Type": "Pass",
"ResultPath": "$.prepareResult",
"Parameters": {
"runs": 0
},
"Next": "Prepare"
},
The parameters property is returned as is and assigned to the path prepareResult which allows it to exist on the next step.
When the StepFunction is executed, the default value of 0 is passed in.
Note: On the PrepareDefaults state, you must set a ResultPath for the parameters to be assigned to, if you don't it will override /all/ the data in the current execution.
Note 2: You can use this method with Choice to say if a value exists go directly to state X, if the value doesn't exist go to Y to default it before going to X