Example 6: Retry Loop
Building on conditional branching, this example adds validation with retry logic—allowing users to correct mistakes before reaching the maximum attempt limit.
Objective
In this example, you'll learn:
- How to validate input format using conditions
- How to implement retry loops with loop-back transitions
- How to track attempt counts with the
incaction - How to exit gracefully after too many failures
- How to provide helpful error feedback with conditional
sayactions
The Scenario
Your contact form collects email addresses, but users sometimes enter invalid formats. You want to:
- Check if the email contains "@" and "."
- Allow up to 3 retry attempts if invalid
- Show helpful error messages on each retry
- Exit gracefully if the user can't provide a valid email
This creates a forgiving experience while preventing infinite loops.
Implementation
Here's the complete tool definition:
{
"type": "context",
"context": {
"task": {
"type": "steps",
"version": "v1alpha",
"id": "contact-form-with-retry",
"tool": {
"name": "submit_contact_info",
"description": "Submit contact form information with email validation"
},
"steps": [
{
"id": "COLLECT_NAME",
"goal": "Collect the user's name",
"instructions": [
"Ask the user for their name.",
"Once you have their name, call the submit_contact_info tool with the user_name parameter."
],
"inputs": [
{
"name": "user_name",
"type": "string",
"description": "The user's name",
"required": true
}
],
"on": {
"enter": [
{"action": "say", "text": "Welcome! Let's collect your contact information."}
],
"submit": [
{"action": "save"}
]
},
"next": [{"id": "COLLECT_EMAIL"}]
},
{
"id": "COLLECT_EMAIL",
"goal": "Collect and validate the user's email address",
"instructions": [
"Ask the user for their email address.",
"The email must contain '@' and '.' to be valid.",
"Once you have their email, call the submit_contact_info tool with the user_email parameter."
],
"inputs": [
{
"name": "user_email",
"type": "string",
"description": "The user's email address (must contain @ and .)",
"required": true
}
],
"on": {
"enter": [
{"action": "set", "name": "local.email_attempts", "value": 0}
],
"presubmit": [
{
"action": "set",
"name": "local.email_is_valid",
"valueFrom": {
"type": "cel",
"expression": "inputs.user_email.contains('@') && inputs.user_email.contains('.')"
}
}
],
"submit": [
{
"action": "inc",
"name": "local.email_attempts",
"if": "!local.email_is_valid"
},
{
"action": "say",
"text": "That doesn't look like a valid email address. Please include an '@' symbol and a domain (e.g., name@example.com).",
"if": "!local.email_is_valid && local.email_attempts < `3`"
},
{
"action": "save",
"if": "local.email_is_valid"
}
]
},
"next": [
{"if": "local.email_is_valid", "id": "COLLECT_TIME"},
{"if": "local.email_attempts >= `3`", "id": "ERROR_TOO_MANY_ATTEMPTS"},
{"id": "COLLECT_EMAIL"}
]
},
{
"id": "COLLECT_TIME",
"goal": "Collect the user's preferred contact time",
"instructions": [
"Ask the user for their preferred contact time.",
"Once you have their preference, call the submit_contact_info tool with the contact_time parameter."
],
"inputs": [
{
"name": "contact_time",
"type": "string",
"description": "The user's preferred contact time",
"required": true,
"enum": ["morning", "afternoon", "evening", "night"]
}
],
"on": {
"submit": [{"action": "save"}]
}
},
{
"id": "ERROR_TOO_MANY_ATTEMPTS",
"goal": "Handle maximum retry attempts exceeded",
"instructions": [
"Apologize that we couldn't validate their email after multiple attempts.",
"Offer to continue without email or suggest they contact support.",
"Call the submit_contact_info tool to complete this step."
],
"inputs": [],
"on": {
"enter": [
{
"action": "say",
"text": "I'm sorry, but I wasn't able to validate your email address after several attempts. You can contact our support team for assistance."
}
]
}
}
]
}
},
"tool": {
"type": "function",
"function": {
"name": "contact_form_retry_workflow",
"description": "Multi-step workflow with email validation and retry logic",
"parameters": {
"type": "object",
"properties": {},
"required": []
}
}
}
}
Key Concepts
Validation with CEL Expressions
The on.presubmit hook runs before validation, allowing you to check custom conditions:
"presubmit": [
{
"action": "set",
"name": "local.email_is_valid",
"valueFrom": {
"type": "cel",
"expression": "inputs.user_email.contains('@') && inputs.user_email.contains('.')"
}
}
]
This uses CEL (Common Expression Language) because:
- CEL supports string methods like
contains() - JMESPath doesn't have built-in string manipulation
The result is stored in local.email_is_valid for use in conditions.
Retry Counter Pattern
Track attempts by incrementing a counter when validation fails:
"on": {
"enter": [
{"action": "set", "name": "local.email_attempts", "value": 0}
],
"submit": [
{
"action": "inc",
"name": "local.email_attempts",
"if": "!local.email_is_valid"
}
]
}
Important: Initialize the counter in on.enter (not on.submit) so it starts at 0 when first entering the step.
Loop-Back Transitions
A step can transition to itself, creating a retry loop:
"next": [
{"if": "local.email_is_valid", "id": "COLLECT_TIME"},
{"if": "local.email_attempts >= `3`", "id": "ERROR_TOO_MANY_ATTEMPTS"},
{"id": "COLLECT_EMAIL"}
]
Evaluation order:
- If valid → proceed to COLLECT_TIME
- If 3+ failed attempts → go to error step
- Otherwise → retry COLLECT_EMAIL
Conditional Error Messages
Use conditional say actions to provide contextual feedback:
{
"action": "say",
"text": "That doesn't look like a valid email. Please include an '@' symbol.",
"if": "!local.email_is_valid && local.email_attempts < `3`"
}
This shows the error message only when:
- The email is invalid, AND
- We haven't hit the max attempts yet
Graceful Error Handling
The ERROR_TOO_MANY_ATTEMPTS step provides a clean exit:
{
"id": "ERROR_TOO_MANY_ATTEMPTS",
"goal": "Handle maximum retry attempts exceeded",
"instructions": [
"Apologize that we couldn't validate their email.",
"Offer alternatives like contacting support."
],
"on": {
"enter": [
{"action": "say", "text": "I'm sorry, but I wasn't able to validate your email..."}
]
}
}
This ensures the workflow ends gracefully rather than looping forever.
How It Works
Successful Validation (First Try)
User: "Hello"
Agent: "Welcome! Let's collect your contact information. What is your name?"
User: "Alice"
Agent: "What is your email address?"
User: "alice@example.com"
[Agent calls submit_contact_info(user_email="alice@example.com")]
→ presubmit: email_is_valid = true (contains @ and .)
→ submit: inc skipped (email is valid), save executes
→ next: email_is_valid → transition to COLLECT_TIME
Agent: "What is your preferred contact time?"
Validation Failure with Retry
User: "alice"
[Agent calls submit_contact_info(user_email="alice")]
→ presubmit: email_is_valid = false (missing @ and .)
→ submit: inc email_attempts → 1
→ submit: say error message
→ next: not valid, attempts < 3 → loop to COLLECT_EMAIL
Agent: "That doesn't look like a valid email address. Please include an '@' symbol and a domain."
User: "alice@"
[Agent calls submit_contact_info(user_email="alice@")]
→ presubmit: email_is_valid = false (missing .)
→ submit: inc email_attempts → 2
→ next: loop to COLLECT_EMAIL
Agent: "That doesn't look like a valid email address..."
User: "alice@example.com"
[Agent calls submit_contact_info(user_email="alice@example.com")]
→ presubmit: email_is_valid = true
→ next: transition to COLLECT_TIME
Agent: "What is your preferred contact time?"
Maximum Attempts Exceeded
User: "alice" (attempt 1)
Agent: "That doesn't look like a valid email..."
User: "alice@" (attempt 2)
Agent: "That doesn't look like a valid email..."
User: "alice.example" (attempt 3)
[Agent calls submit_contact_info(user_email="alice.example")]
→ presubmit: email_is_valid = false
→ submit: inc email_attempts → 3
→ next: attempts >= 3 → transition to ERROR_TOO_MANY_ATTEMPTS
Agent: "I'm sorry, but I wasn't able to validate your email address after several attempts. You can contact our support team for assistance."
Try It
To test this workflow in the Syllable Console:
- Create a new tool with the JSON above
- Assign it to an agent
- Test various scenarios:
- Valid email on first try (
test@example.com) - Invalid email then valid (
test→test@example.com) - Three invalid emails to trigger error step
- Valid email on first try (
Verify the retry behavior: Each invalid email should show an error message and allow another attempt.
What's Next
This example handles validation within the workflow. In Example 7: Tool Integration, you'll learn how to:
- Call external tools during workflow execution
- Control which tools are available per step
- Implement progressive tool disclosure for security

