> For the complete documentation index, see [llms.txt](https://gdplabs.gitbook.io/catapa/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gdplabs.gitbook.io/catapa/developer-documentation/digital-employee/advanced-examples/user-information-in-digital-employee-runs.md).

# User Information in Digital Employee Runs

### Overview

Digital Employee Core handles user information in two different ways:

* **User-scoped memory** uses `memory_user_id` to isolate remembered facts per user.
* **User-authenticated tools** use delegated user tokens so external systems can authorize actions as the current user.

These two mechanisms are related, but they solve different problems. `memory_user_id` controls memory isolation. Delegation tokens control what a tool is allowed to do on behalf of the user.

For the GL AIP delegation flow, see:

* [Delegate to Agent](https://gdplabs.gitbook.io/sdk/gl-identity-and-access-management/tutorials/agent-iam/agent-authentication/delegate-to-agent) for the delegation token object and scope structure.
* [Validate Delegation Token](https://gdplabs.gitbook.io/sdk/gl-identity-and-access-management/tutorials/agent-iam/agent-authentication/validate-delegation-token) for how receiving services validate delegation tokens and enforce scopes.

For GL Connectors integration setup, see [Integration Setup](https://gdplabs.gitbook.io/sdk/gl-connectors/sdk/api/in-depth-setup/integration-setup).

### Key Concepts

#### `memory_user_id`

`memory_user_id` is the stable user identifier used by the memory provider.

* Same `memory_user_id` + same agent => the agent can recall that user's previous facts.
* Different `memory_user_id` + same agent => memory stays isolated.
* It should be a stable internal user ID. Avoid PII such as email addresses unless required.

See also: [Memory Configuration](/catapa/developer-documentation/digital-employee/advanced-examples/memory-configuration.md).

#### `user_authentication`

`user_authentication` is an opt-in flag in a tool config. When it is enabled, GL AIP knows that the dependency needs a delegated user token.

Use it for tools that call user-scoped APIs, such as calendars, mail, HR self-service, finance self-service, GL Connectors, or other systems where permissions depend on the current user.

#### Delegated tokens

Delegated tokens are passed at run time, not written into prompts. GL AIP validates the incoming delegation token, resolves which downstream token is needed for each user-authenticated dependency, then exposes that token to tools through runtime metadata.

For example, a GL Connectors-enabled dependency receives `gl_connectors_token` in `RunnableConfig.metadata`. For local or manual runs, you can pass the token explicitly to `run()`:

```python
result = digital_employee.run(
    message="Show my pending requests",
    gl_connectors_token=os.getenv("GL_CONNECTORS_TOKEN"),
)
```

The token name depends on the downstream integration. For GL Connectors, use `gl_connectors_token`.

#### GL AIP handoff

Tools should not parse the original GL IAM delegation token object directly. Treat GL AIP as the boundary that:

1. Receives the delegation token object described in [Delegate to Agent](https://gdplabs.gitbook.io/sdk/gl-identity-and-access-management/tutorials/agent-iam/agent-authentication/delegate-to-agent).
2. Validates the delegation token as described in [Validate Delegation Token](https://gdplabs.gitbook.io/sdk/gl-identity-and-access-management/tutorials/agent-iam/agent-authentication/validate-delegation-token).
3. Looks up user-authenticated dependencies and attaches the appropriate integration token, such as `gl_connectors_token`, to tool runtime metadata.

In custom tools, read only the integration-specific token from `RunnableConfig.metadata` and use it to authenticate with the downstream connector.

When using GL Connectors, configure the connector integration first. See [GL Connectors Integration Setup](https://gdplabs.gitbook.io/sdk/gl-connectors/sdk/api/in-depth-setup/integration-setup).

### Runtime User Context in `RunnableConfig`

Digital Employee tools can receive user and conversation context through `RunnableConfig.metadata`. This context can be used by the tools to act on behalf of human (OBOH).

The runtime parameter contract is sent by GLChat to AIP from the [AIP execution strategy](https://github.com/GDP-ADMIN/glchat/blob/main/applications/glchat-be/glchat_be/agent/strategy/aip_execution_strategy.py).

The main chat-message path populates this context from the [Agent message processor](https://github.com/GDP-ADMIN/glchat/blob/main/applications/glchat-be/glchat_be/api/helper/message/processor/agent_message_processor.py). Pipeline-based runs may provide context through the [Pipeline service](https://github.com/GDP-ADMIN/glchat/blob/main/applications/glchat-be/glchat_be/agent/service/pipeline_service.py).

| Parameter             | Location                                      | Purpose                                         |
| --------------------- | --------------------------------------------- | ----------------------------------------------- |
| `gl_connectors_token` | `RunnableConfig.metadata.gl_connectors_token` | Delegated connector token for the current user. |
| `user_id`             | `RunnableConfig.metadata.user_id`             | Current user identifier.                        |
| `tenant_id`           | `RunnableConfig.metadata.tenant_id`           | Current tenant context.                         |
| `conversation_id`     | `RunnableConfig.metadata.conversation_id`     | Conversation associated with the run.           |
| `message_id`          | `RunnableConfig.metadata.message_id`          | User message associated with the run.           |
| `email`               | `RunnableConfig.metadata.email`               | Current user email or username fallback.        |
| `organization_id`     | `RunnableConfig.metadata.organization_id`     | Organization context.                           |
| `chatbot_id`          | `RunnableConfig.metadata.chatbot_id`          | Chatbot or assistant identifier.                |
| `agent`               | `RunnableConfig.metadata.agent`               | Agent-specific metadata, when provided.         |

Example usage in a custom tool:

```python
def _run(self, config: RunnableConfig = None, **kwargs):
    metadata = ((config or {}).get("metadata") or {})

    gl_connectors_token = metadata.get("gl_connectors_token")
    user_id = metadata.get("user_id")
    tenant_id = metadata.get("tenant_id")
    conversation_id = metadata.get("conversation_id")
    message_id = metadata.get("message_id")
    email = metadata.get("email")
    organization_id = metadata.get("organization_id")
    chatbot_id = metadata.get("chatbot_id")
    agent_metadata = metadata.get("agent")
```

Do not store these values in tool configuration or prompts. Treat them as runtime context for the current Digital Employee run.

### User-Scoped Memory Example

Enable memory in `agent_config`, then pass `memory_user_id` on each `run()` or `arun()` call.

{% code lineNumbers="true" %}

```python
from digital_employee_core import DigitalEmployee, DigitalEmployeeIdentity, DigitalEmployeeJob
from digital_employee_core.configuration.agent_configuration import AgentConfigKeys, MemoryProvider

job = DigitalEmployeeJob(
    title="Memory-Enabled Assistant",
    description="A digital employee that can remember user-specific facts across calls",
    instruction="When the user tells you a personal preference or fact, remember it for future conversation.",
)

identity = DigitalEmployeeIdentity(name="memory_assistant", email="memory.assistant@example.com", job=job)

digital_employee = DigitalEmployee(
    identity=identity,
    agent_config={AgentConfigKeys.MEMORY: MemoryProvider.MEM0},
)

digital_employee.deploy()

memory_user_id = "user-123"

digital_employee.run(
    message="My preferred report format is a short bullet summary. Please remember this.",
    memory_user_id=memory_user_id,
)

digital_employee.run(
    message="How should you format my reports?",
    memory_user_id=memory_user_id,
)
```

{% endcode %}

> **Note:** If memory is enabled, `memory_user_id` is required. Digital Employee Core raises an error when `run()` or `arun()` is called without it.

### User Information in Custom Tools

Custom tools should treat delegated user tokens as runtime credentials. Do not store them in static config and do not include them in prompts.

Digital Employee Core custom tools follow the LangChain `BaseTool` pattern:

* Define an input schema with Pydantic.
* Define a tool config schema with Pydantic.
* Set `tool_config_schema` on the tool.
* Read static config with `self.get_tool_config(config)`.
* Read delegated user tokens from `RunnableConfig.metadata`, such as `config.get("metadata").get("gl_connectors_token")`.

#### 1) Create a custom tool

This example creates a custom `user_profile_tool` that calls GL Connectors to fetch the current user's profile. The API key is service-level authentication. The `gl_connectors_token` is delegated user authentication for the current run.

Before using this pattern, ensure the target connector integration is configured in GL Connectors. See [Integration Setup](https://gdplabs.gitbook.io/sdk/gl-connectors/sdk/api/in-depth-setup/integration-setup).

{% code lineNumbers="true" %}

```python
import json
import requests
from typing import Any

from gl_connectors_sdk import GLConnectors
from langchain_core.runnables import RunnableConfig
from langchain_core.tools import BaseTool
from pydantic import BaseModel, Field

REQUEST_TIMEOUT_SECONDS = 30


class UserProfileToolInput(BaseModel):
    """Input schema for user profile tool."""

    include_contact: bool = Field(default=False, description="Whether to include contact fields in the response.")


class UserProfileToolConfig(BaseModel):
    """Configuration schema for user profile tool."""

    user_api_base_url: str = Field(description="The base URL for the downstream user profile API.")
    gl_connectors_api_base_url: str = Field(description="The base URL for the GL Connectors API.")
    gl_connectors_api_key: str = Field(description="The API key for authenticating with the GL Connectors API.")
    user_authentication: bool = Field(description="Whether this tool requires delegated user authentication.", default=True)


class UserProfileTool(BaseTool):
    """Tool for reading the current user's profile through GL Connectors."""

    name: str = "user_profile_tool"
    description: str = "Read the current user's profile information."
    args_schema: type[BaseModel] = UserProfileToolInput
    tool_config_schema: type[BaseModel] = UserProfileToolConfig

    def _run(self, include_contact: bool = False, config: RunnableConfig = None, **_kwargs: Any) -> str:
        """Read the current user's profile."""
        tool_config = self.get_tool_config(config)
        gl_connectors_token = ((config or {}).get("metadata") or {}).get("gl_connectors_token")

        if not gl_connectors_token:
            return "Error: gl_connectors_token is required for this user-authenticated tool."

        try:
            access_token = self._get_access_token_from_gl_connectors(tool_config, gl_connectors_token)
            headers = {"Authorization": f"Bearer {access_token}"}
            params = {"include_contact": include_contact}

            base_url = tool_config.user_api_base_url.rstrip("/")
            url = f"{base_url}/user/profile"
            response = requests.get(url, headers=headers, params=params, timeout=REQUEST_TIMEOUT_SECONDS)
            response.raise_for_status()
            return response.text
        except requests.HTTPError as e:
            return f"Failed to read user profile. Status code: {e.response.status_code}, Response: {e.response.text}"
        except Exception as e:
            return f"Error reading user profile: {str(e)}"

    def _get_access_token_from_gl_connectors(
        self,
        tool_config: UserProfileToolConfig,
        gl_connectors_token: str,
    ) -> str:
        """Exchange the delegated GL Connectors token for downstream auth info."""
        connector = GLConnectors(
            api_base_url=tool_config.gl_connectors_api_base_url,
            api_key=tool_config.gl_connectors_api_key,
        )

        # Check which integration belongs to this delegated user.
        user_info = connector.get_user_info(gl_connectors_token)
        user_identifier = next(
            integration.user_identifier
            for integration in user_info.integrations
            if integration.connector == "user-profile"
        )

        # GL Connectors returns integration-specific auth information as auth_string.
        integration_info = connector.get_integration("user-profile", gl_connectors_token, user_identifier)
        auth_string = integration_info.get("auth_string")
        if not auth_string:
            raise ValueError("auth_string is missing or empty")

        return json.loads(auth_string)["access_token"]
```

{% endcode %}

#### 2) Add `user_authentication` in tool config

Set `user_authentication: true` in the tool config template. This tells GL AIP that the tool needs delegated user authentication.

`config_templates/tool_configs.yaml`:

```yaml
user_profile_tool:
  user_api_base_url: "${USER_API_BASE_URL}"
  gl_connectors_api_base_url: "${GL_CONNECTORS_API_BASE_URL}"
  gl_connectors_api_key: "${GL_CONNECTORS_API_KEY}"
  user_authentication: true
```

The config key must match the tool name:

```python
class UserProfileTool(BaseTool):
    name: str = "user_profile_tool"
```

#### 3) Handle delegated tokens in tool logic

The delegated token is passed to `digital_employee.run()` and then forwarded to the tool call through `RunnableConfig.metadata`. In the custom tool, read it from `config.get("metadata").get("gl_connectors_token")`.

```python
def _run(self, include_contact: bool = False, config: RunnableConfig = None, **_kwargs: Any) -> str:
    tool_config = self.get_tool_config(config)
    gl_connectors_token = ((config or {}).get("metadata") or {}).get("gl_connectors_token")

    if not gl_connectors_token:
        return "Error: gl_connectors_token is required for this user-authenticated tool."

    access_token = self._get_access_token_from_gl_connectors(tool_config, gl_connectors_token)

    headers = {
        "Authorization": f"Bearer {access_token}",
    }
```

Use the GL Connectors token to retrieve the integration `auth_string`, then parse the auth string for the downstream API credential. The connector and its integration must already be set up in GL Connectors. See [Integration Setup](https://gdplabs.gitbook.io/sdk/gl-connectors/sdk/api/in-depth-setup/integration-setup).

```python
def _get_access_token_from_gl_connectors(
    self,
    tool_config: UserProfileToolConfig,
    gl_connectors_token: str,
) -> str:
    connector = GLConnectors(
        api_base_url=tool_config.gl_connectors_api_base_url,
        api_key=tool_config.gl_connectors_api_key,
    )

    user_info = connector.get_user_info(gl_connectors_token)
    user_identifier = next(
        integration.user_identifier
        for integration in user_info.integrations
        if integration.connector == "user-profile"
    )

    integration_info = connector.get_integration("user-profile", gl_connectors_token, user_identifier)
    auth_string = integration_info.get("auth_string")
    if not auth_string:
        raise ValueError("auth_string is missing or empty")

    return json.loads(auth_string)["access_token"]
```

In this pattern:

* `gl_connectors_api_key` authenticates the tool to GL Connectors.
* `gl_connectors_token` identifies the current delegated user in GL Connectors.
* `auth_string` contains the downstream integration credential for that delegated user.
* `user_authentication: true` signals that GL AIP should delegate the appropriate token to the tool.

For GL Connectors setup details, see [GL Connectors Integration Setup](https://gdplabs.gitbook.io/sdk/gl-connectors/sdk/api/in-depth-setup/integration-setup).

### Configuration Propagation to Sub-Agents

Digital Employee Core propagates tool configs from the parent Digital Employee to sub-agents by dependency name.

This means a parent can define `user_authentication: true` once for a shared tool, and a sub-agent that uses the same dependency can receive the config automatically.

{% code lineNumbers="true" %}

```python
digital_employee = DigitalEmployee(
    identity=identity,
    tools=[Tool.from_langchain(UserProfileTool)],
    sub_agents=[profile_helper_agent],
    configurations=configurations,
)

digital_employee.deploy()
```

{% endcode %}

If the sub-agent defines its own config for the same tool, the sub-agent config takes precedence.

See also: [Sub-Agents Configuration](/catapa/developer-documentation/digital-employee/advanced-examples/sub-agents-configuration.md).

### Integrate the Custom Tool in Digital Employee

Wrap the custom tool with `Tool.from_langchain()` and add a config loader for the tool config template.

For deployed GL AIP runs, AIP validates the delegation token and attaches integration-specific tokens to runtime metadata. For local or manual runs, pass the delegated token explicitly in `run()`.

{% code lineNumbers="true" %}

```python
import os
from pathlib import Path

from dotenv import load_dotenv
from glaip_sdk import MCP, Agent, Tool

from digital_employee_core import (
    DEFAULT_MODEL_NAME,
    ConfigTemplateLoader,
    DigitalEmployee,
    DigitalEmployeeConfiguration,
    DigitalEmployeeIdentity,
    DigitalEmployeeJob,
)

from my_project.tools.user_profile_tool import UserProfileTool

load_dotenv()


class MyDigitalEmployee(DigitalEmployee):
    """Digital Employee with custom tool config templates."""

    def __init__(
        self,
        identity: DigitalEmployeeIdentity,
        tools: list[Tool] | None = None,
        sub_agents: list[Agent] | None = None,
        mcps: list[MCP] | None = None,
        configurations: list[DigitalEmployeeConfiguration] | None = None,
        model: str | None = DEFAULT_MODEL_NAME,
    ):
        super().__init__(identity, tools, sub_agents, mcps, configurations, model)

        config_dir = Path(__file__).parent / "config_templates"
        self.add_config_loader(ConfigTemplateLoader(template_dir=config_dir))


identity = DigitalEmployeeIdentity(
    name="profile_assistant",
    email="profile.assistant@example.com",
    job=DigitalEmployeeJob(
        title="Profile Assistant",
        description="Helps users retrieve their own profile information",
        instruction="Use the user profile tool when the user asks about their own profile.",
    ),
)

configurations = [
    DigitalEmployeeConfiguration(key="USER_API_BASE_URL", value=os.getenv("USER_API_BASE_URL", "")),
    DigitalEmployeeConfiguration(key="GL_CONNECTORS_API_BASE_URL", value=os.getenv("GL_CONNECTORS_API_BASE_URL", "")),
    DigitalEmployeeConfiguration(key="GL_CONNECTORS_API_KEY", value=os.getenv("GL_CONNECTORS_API_KEY", "")),
]

# GL_CONNECTORS_TOKEN is a delegated user token for the current run.
# See the GL AIP Delegate to Agent guide for how to obtain it.
gl_connectors_token = os.getenv("GL_CONNECTORS_TOKEN")

digital_employee = MyDigitalEmployee(
    identity=identity,
    tools=[Tool.from_langchain(UserProfileTool)],
    configurations=configurations,
)

digital_employee.deploy()

result = digital_employee.run(
    message="Show my profile information",
    gl_connectors_token=gl_connectors_token,
)
```

{% endcode %}

### Notes / Best Practices

* **Do not put tokens in prompts.** Pass delegated tokens as `run()` / `arun()` keyword arguments only for local/manual runs; in deployed GL AIP runs, read the resolved token from `RunnableConfig.metadata`.
* **Do not parse GL IAM delegation tokens in custom tools.** Use GL AIP for delegation-token validation and only consume the integration-specific token exposed to the tool.
* **Use `memory_user_id` only for memory scoping.** Do not use it as an authorization token.
* **Enable `user_authentication` only when needed.** Tools that only use service credentials do not need delegated user tokens.
* **Keep service credentials in configuration.** Use `DigitalEmployeeConfiguration`, config templates or environment variables.
* **Propagate configs intentionally.** Shared parent configs are convenient for sub-agents, but sub-agent-specific configs should be explicit when permissions differ.
* **Validate original delegation tokens at service boundaries.** If a receiving service handles the original GL IAM delegation token directly, validate it according to [Validate Delegation Token](https://gdplabs.gitbook.io/sdk/gl-identity-and-access-management/tutorials/agent-iam/agent-authentication/validate-delegation-token).

### Troubleshooting

#### Memory is enabled but the run fails

Check that every `run()` or `arun()` call includes a non-empty `memory_user_id`.

```python
digital_employee.run(
    message="What did I tell you earlier?",
    memory_user_id="user-123",
)
```

#### Tool does not receive the user token

Check that:

1. The tool config includes `user_authentication: true`.
2. The run call passes the integration token, for example `gl_connectors_token`.
3. The token is available in the environment when running locally.

   ```bash
   export GL_CONNECTORS_TOKEN="<delegated_token>"
   ```
4. The `config` argument in the tool's `_run` or `_arun` method is annotated as exactly `RunnableConfig`. Do not use a union type: LangChain's runtime config injection does not recognize `RunnableConfig | None`.

   Use:

   ```python
   def _run(self, config: RunnableConfig = None, **kwargs):
       gl_connectors_token = ((config or {}).get("metadata") or {}).get("gl_connectors_token")
   ```

   Do not use:

   ```python
   def _run(self, config: RunnableConfig | None = None, **kwargs):
       ...
   ```
