Skip to main content
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",
                "value_from": {
                  "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., [email protected]).",
                "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",
    "value_from": {
      "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: "[email protected]"

[Agent calls submit_contact_info(user_email="[email protected]")]
  → 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: "[email protected]"

[Agent calls submit_contact_info(user_email="[email protected]")]
  → 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:
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