Home
Developer documentation

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 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:

{
  "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:

  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:

{
  "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:

  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 (testtest@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, you'll learn how to:

  • Call external tools during workflow execution
  • Control which tools are available per step
  • Implement progressive tool disclosure for security

Next steps

Continue with the next guide, or explore another part of the documentation.

Example 7: Tool Integration

Integrate external APIs and implement progressive tool disclosure patterns

Explore further: