> ## Documentation Index
> Fetch the complete documentation index at: https://docs.syllable.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Example 6: Retry Loop

> Add validation with retry attempts and graceful error handling

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 `inc` action
* How to exit gracefully after too many failures
* How to provide helpful error feedback with conditional `say` actions

***

## The Scenario

Your contact form collects email addresses, but users sometimes enter invalid formats. You want to:

1. Check if the email contains "@" and "."
2. Allow up to 3 retry attempts if invalid
3. Show helpful error messages on each retry
4. 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:

```json theme={null}
{
  "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:

```json theme={null}
"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:

```json theme={null}
"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:

```json theme={null}
"next": [
  {"if": "local.email_is_valid", "id": "COLLECT_TIME"},
  {"if": "local.email_attempts >= `3`", "id": "ERROR_TOO_MANY_ATTEMPTS"},
  {"id": "COLLECT_EMAIL"}
]
```

Evaluation order:

1. If valid → proceed to COLLECT\_TIME
2. If 3+ failed attempts → go to error step
3. Otherwise → retry COLLECT\_EMAIL

### Conditional Error Messages

Use conditional `say` actions to provide contextual feedback:

```json theme={null}
{
  "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:

```json theme={null}
{
  "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:

1. Create a new tool with the JSON above
2. Assign it to an agent
3. 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

**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](./example-07-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
