> ## 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 7: Tool Integration

> Integrate external APIs and implement progressive tool disclosure patterns

The final tutorial example demonstrates external tool integration—calling APIs during workflow execution and controlling which tools are available at each step.

***

## Objective

In this example, you'll learn:

* How to use the `call` action to invoke external tools
* How to use `tools.allow` for progressive tool disclosure
* How to use `tools.call` to force tool execution
* How to integrate external validation requests and notifications into your workflow

***

## The Scenario

Your contact form needs to integrate with external systems:

1. **Request email-domain validation** using an external API
2. **Send a notification to CRM** when the form is completed
3. **Restrict tool access** during sensitive data collection steps

This creates a production-ready workflow with real integrations.

***

## Implementation

Here's the complete tool definition:

```json theme={null}
{
  "type": "context",
  "context": {
    "task": {
      "type": "steps",
      "version": "v1alpha",
      "id": "contact-form-with-integration",
      "tool": {
        "name": "submit_contact_info",
        "description": "Submit contact form information with external integrations"
      },
      "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
            }
          ],
          "tools": {
            "allow": ["submit_contact_info"]
          },
          "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.",
            "Once you have their email, call the submit_contact_info tool with the user_email parameter.",
            "The system will validate the email domain automatically."
          ],
          "inputs": [
            {
              "name": "user_email",
              "type": "string",
              "description": "The user's email address",
              "required": true
            }
          ],
          "tools": {
            "allow": ["submit_contact_info", "validate_email_domain"]
          },
          "on": {
            "submit": [
              {
                "action": "call",
                "name": "validate_email_domain",
                "arguments": {
                  "email": "{{inputs.user_email}}"
                }
              },
              {"action": "save"}
            ]
          },
          "next": [{"id": "COLLECT_TIME"}]
        },
        {
          "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"]
            }
          ],
          "tools": {
            "allow": ["submit_contact_info"]
          },
          "on": {
            "submit": [
              {"action": "save"}
            ]
          },
          "next": [{"id": "CONFIRM_AND_NOTIFY"}]
        },
        {
          "id": "CONFIRM_AND_NOTIFY",
          "goal": "Confirm information and send CRM notification",
          "instructions": [
            "Summarize the collected information for the user:",
            "- Name: {{user_name}}",
            "- Email: {{user_email}}",
            "- Preferred contact time: {{contact_time}}",
            "Ask them to confirm, then call the submit_contact_info tool to complete."
          ],
          "inputs": [],
          "tools": {
            "allow": ["submit_contact_info", "send_crm_notification"],
            "call": true
          },
          "on": {
            "enter": [
              {
                "action": "call",
                "name": "send_crm_notification",
                "arguments": {
                  "name": "{{user_name}}",
                  "email": "{{user_email}}",
                  "contact_time": "{{contact_time}}",
                  "source": "contact_form_workflow"
                }
              }
            ]
          }
        }
      ]
    }
  },
  "tool": {
    "type": "function",
    "function": {
      "name": "contact_form_integration_workflow",
      "description": "Multi-step workflow with external tool integration",
      "parameters": {
        "type": "object",
        "properties": {},
        "required": []
      }
    }
  }
}
```

***

## Key Concepts

### The `call` Action

The `call` action invokes an external tool from a lifecycle hook. Cortex automatically decides the routing path based on whether the `arguments` supply all of the target tool's required parameters:

```json theme={null}
{
  "action": "call",
  "name": "validate_email_domain",
  "arguments": {
    "email": "{{inputs.user_email}}"
  }
}
```

**Auto-decide routing:**

| Required params supplied?      | Route                  | What happens                                                                                            |
| ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------- |
| All present (or tool has none) | **Inject** (synthetic) | Tool call bypasses the LLM and executes in a single turn.                                               |
| Some missing                   | **Hint** (LLM-guided)  | A `pending_tool_call` is queued; the LLM generates the call on the next turn with forced `tool_choice`. |

In the `COLLECT_EMAIL` example above, `validate_email_domain` requires one param (`email`) and `arguments` provides it via template — so Cortex takes the **inject** route: the tool executes synthetically without LLM involvement.

If you omitted the `email` key from `arguments`, Cortex would take the **hint** route: the LLM would see a `pending_tool_call` hint and generate the call itself.

**Important:** The workflow engine does not automatically copy external tool results into `inputs.*` or `local.*`. If later routing depends on a tool result, add a follow-up submit step that captures normalized result fields, or rely on endpoint-persisted `vars.*`.

**Template variables in arguments:**

* `{{inputs.user_email}}` — Current step input
* `{{user_name}}` — Global variable (saved from earlier step)
* `{{local.counter}}` — Task-local variable

### Progressive Tool Disclosure with `tools.allow`

Control which tools are visible at each step:

```json theme={null}
// COLLECT_NAME: Only submit tool (no external tools needed)
"tools": {
  "allow": ["submit_contact_info"]
}

// COLLECT_EMAIL: Submit + email validation tool
"tools": {
  "allow": ["submit_contact_info", "validate_email_domain"]
}

// CONFIRM_AND_NOTIFY: Submit + CRM notification tool
"tools": {
  "allow": ["submit_contact_info", "send_crm_notification"]
}
```

**Benefits:**

* **Security**: Prevent access to sensitive tools during early steps
* **Focus**: Agent only sees relevant tools for the current task
* **Progressive disclosure**: Unlock capabilities as the workflow advances

**Note:** The submit tool is always available, regardless of `tools.allow`.

### Auto-Advancing with `tools.call`

`tools.call: true` forces the LLM to produce a tool call on turns where no pending `call` action exists. In practice, this forces the **submit tool** — making it the standard mechanism for auto-advancing bridge steps.

`tools.call` and `call` actions operate on **different turns** and compose naturally:

```json theme={null}
{
  "id": "CONFIRM_AND_NOTIFY",
  "tools": {
    "allow": ["submit_contact_info", "send_crm_notification"],
    "call": true
  },
  "on": {
    "enter": [
      {"action": "call", "name": "send_crm_notification", "arguments": {...}}
    ]
  }
}
```

**What happens:**

1. Step is entered, `on.enter` fires the `call` action
2. `send_crm_notification` has all required params provided → **inject** route: tool executes synthetically (single turn)
3. On the next turn, no pending call remains → `tools.call: true` forces the submit tool
4. The step submits and the workflow advances

If the `call` action took the **hint** route instead (missing required params), the sequence would be: turn 1 queues the hint → turn 2 the LLM generates the tool call (forced `tool_choice`) → turn 3 `tools.call: true` forces submit.

**Use cases for `tools.call: true`:**

* Auto-advancing bridge steps after a `call` action completes
* Forcing submit on terminal steps with no inputs
* Making zero-input steps fully deterministic

### External Tool Assumptions

This example assumes two external tools are available:

**1. `validate_email_domain`**

```json theme={null}
{
  "name": "validate_email_domain",
  "description": "Validates that an email domain exists and accepts mail",
  "parameters": {
    "properties": {
      "email": {"type": "string", "description": "Email to validate"}
    },
    "required": ["email"]
  }
}
```

Returns: `{"valid": true/false, "domain": "example.com", "message": "..."}`

**2. `send_crm_notification`**

```json theme={null}
{
  "name": "send_crm_notification",
  "description": "Sends a lead notification to the CRM system",
  "parameters": {
    "properties": {
      "name": {"type": "string"},
      "email": {"type": "string"},
      "contact_time": {"type": "string"},
      "source": {"type": "string"}
    },
    "required": ["name", "email"]
  }
}
```

Returns: `{"success": true, "lead_id": "12345"}`

***

## How It Works

### Step-by-Step Flow

```
User: "Hello"

[Platform enters COLLECT_NAME]
  → tools.allow: only submit_contact_info visible
  → on.enter: say "Welcome!"

Agent: "Welcome! Let's collect your contact information. What is your name?"

User: "Alice"

[Agent calls submit_contact_info(user_name="Alice")]
  → on.submit: save user_name
  → Transition to COLLECT_EMAIL
  → tools.allow: submit + validate_email_domain visible

Agent: "What is your email address?"

User: "alice@example.com"

[Agent calls submit_contact_info(user_email="alice@example.com")]
  → on.submit: call validate_email_domain(email="alice@example.com")
  → Required param "email" provided → inject route (synthetic, single turn)
  → Tool executes, response available to the agent
  → on.submit: save user_email
  → Transition to COLLECT_TIME

Agent: "Thanks. What is your preferred contact time?"

User: "morning"

[Agent calls submit_contact_info(contact_time="morning")]
  → on.submit: save contact_time
  → Transition to CONFIRM_AND_NOTIFY
  → on.enter: call send_crm_notification(...)
  → All required params provided → inject route (synthetic, single turn)
  → CRM receives notification
  → Next turn: no pending call → tools.call: true forces submit

Agent: "I've recorded your information and notified our team. Here's what we have:
- Name: Alice
- Email: alice@example.com
- Preferred contact time: morning

Is this correct?"

User: "Yes"

[Agent calls submit_contact_info()]
  → No next step (terminal)
  → Workflow completes

Agent: "Thank you! We'll be in touch soon."
```

### Tool Visibility Per Step

| Step                 | Available Tools                                |
| -------------------- | ---------------------------------------------- |
| COLLECT\_NAME        | `submit_contact_info`                          |
| COLLECT\_EMAIL       | `submit_contact_info`, `validate_email_domain` |
| COLLECT\_TIME        | `submit_contact_info`                          |
| CONFIRM\_AND\_NOTIFY | `submit_contact_info`, `send_crm_notification` |

***

## Best Practices

### 1. Minimize Tool Access

Only expose tools that are needed for the current step:

```json theme={null}
// Good: Restricted to what's needed
"tools": {"allow": ["submit_contact_info", "lookup_patient"]}

// Avoid: All tools visible when only one is needed
"tools": {}  // null = all tools
```

### 2. Use `call` for Mandatory Operations

When a tool must be called (validation, notification), use the `call` action:

```json theme={null}
"on": {
  "submit": [
    {"action": "call", "name": "log_audit_event", "arguments": {...}}
  ]
}
```

### 3. Combine `tools.call` with `on.enter` for Data Fetching

When you need data before the step can proceed:

```json theme={null}
{
  "id": "SHOW_ACCOUNT_BALANCE",
  "tools": {"call": true},
  "on": {
    "enter": [
      {"action": "call", "name": "get_account_balance", "arguments": {"id": "{{account_id}}"}}
    ]
  }
}
```

### 4. Handle Tool Failures Gracefully

External tools can fail. Consider adding error handling steps:

```json theme={null}
"next": [
  {"if": "local.validation_passed", "id": "NEXT_STEP"},
  {"if": "local.validation_failed", "id": "HANDLE_ERROR"},
  {"id": "RETRY"}
]
```

### 5. Normalize Tool Results Before Branching

If a future step needs structured data from an external tool, do not assume the result is automatically written into workflow state. Instead:

1. queue the external tool call,
2. let the agent read the tool result,
3. have the agent resubmit normalized fields such as `validation_status` or `directory_name`,
4. branch only on `inputs.*`, `local.*`, or known `vars.*`.

***

## Try It

To test this workflow, you'll need:

1. The workflow JSON above assigned to an agent
2. External tools (`validate_email_domain`, `send_crm_notification`) available in your environment

**Note:** If external tools aren't available, you can modify the example to remove the `call` actions and test the `tools.allow` behavior independently.

***

## Summary

Congratulations! You've completed the Step Workflows tutorial. You now know how to:

1. **Create basic workflows** with steps and terminal states (Example 1)
2. **Collect and validate inputs** with automatic schema generation (Example 2)
3. **Chain multiple steps** with transitions and data persistence (Example 3)
4. **Add polish** with lifecycle actions, say messages, and counters (Example 4)
5. **Route conditionally** based on user input (Example 5)
6. **Handle validation failures** with retry loops (Example 6)
7. **Integrate external tools** with progressive disclosure (Example 7)

For complete reference documentation, see [Step Workflow Reference](./reference).
