---
title: Google Analytics 4 Agent Platform tools guide
slug: optimizely-connect-platform/docs/google-analytics-agent-platform-tool-end-to-end-guide
docTags: 
createdAt: 2026-07-23T12:37:03.000Z
---

This guide helps you build an Optimizely Connect Platform (OCP) app that implements Optimizely Agent Platform tools. Optimizely based this implementation on the Optimizely-developed and publicly available Google Analytics 4 Agent Platform tool. Access the tool in the OCP App Directory or directly through Mark.

Optimizely simplified and modified the code, removing internal references, such as references to internal monitoring tools. View the full source code of the reference app on [Github](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app).

## Plan

### Requirements

- Define app functionality before starting. The app aims to build tools enabling Mark to access Google Analytics 4 data and request reports based on natural language prompts from users
  For example, if you provide the following prompt, Mark constructs a tool call with the required parameters and fetches results.

```javascript
Show me top pages visited via clickthroughs from chatgpt.com from last month
```

- Define clear evaluation criteria by using a set of prompts. This is helpful due to the open-ended nature of AI agentic platforms.
  Optimizely's evaluation prompts include the following, which validate tool performance with Mark:
  - `Show me how many pageviews our homepage had last month`
  - `How many visits from the Black Friday Facebook campaign did we get last month`
  - `What is the engagement rate with our pricing page?`

### Design

With defined requirements, start designing the app. Critical decisions include the following:

- Main tools implemented by the app
- Supporting tools
- The API to fulfil requests
- App authorization to the API
- Additional app-level settings

The app accesses Google Analytics Data through the [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1). Optimizely uses two main methods from this API:

- [`runReport`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runReport) –Returns a customized report of your Google Analytics event data. This is the preferred method for [simple report queries](https://developers.google.com/analytics/devguides/reporting/data/v1/basics).
- [`runRealtimeReport`](https://developers.google.com/analytics/devguides/reporting/data/v1/rest/v1beta/properties/runRealtimeReport) –  Gives Mark access to data from the present moment to 30 minutes ago (60 minutes for Google Analytics 360 properties).

Additionally, Optimizely implements the following supporting tools to ensure Mark uses the main tools correctly:

- `get_property_id` – Returns the configured GA4 property ID, preventing Mark from asking users.
- `lookup_dimension_metric_names` – Translates user-friendly display names, such as *Active Users*, to API names such as `activeUsers`.
- `get_dimensions` – Lists all standard dimensions available for the property.
- `get_metrics` – Lists all standard metrics available for the property.
- `get_custom_dimensions_and_metrics` – Lists custom dimensions and metrics configured for the property.

Use these supporting tools to ensure a positive user experience. Without them, users require technical knowledge of API names and property IDs, creating friction.

The Google Analytics Data API offers various invocation methods. For this app, the most convenient method is to use the official [Google Analytics Data: Node.js Client](https://googleapis.dev/nodejs/analytics-data/latest/index.html#installing-the-client-library).

Google Analytics Data API supports multiple authorization methods. Use a method suitable for service-to-service communication with non-expiring tokens, as the app works outside of user sessions. The API offers two such methods, [service account auth](https://developers.google.com/analytics/devguides/config/userdeletion/v3/authorization#service_accounts) and [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials#personal). The app implements both, letting users choose based on their preference and governance rules.

:::hint{type="warning"}
**🚧 Important**
Authorization to Google Analytics Data API is organization-based. All users within an Optimizely customer organization use the same credentials. This is suitable to access reports. However, apps requiring per-user authorization must use [custom OAuth providers](https://www.npmjs.com/package/@optimizely-opal/opal-tool-ocp-sdk).
:::

The app also lets users define a custom `hostname` filter. When configured at the OCP account level, all queries to Google Analytics include this filter. This is useful when an organization hosts multiple hostnames in the same Google Analytics property, but Mark queries work in the context of one hostname.

In summary, the key design decisions include:

- **Main tools to implement** – Implement `run_report`, `run_realtime_report`.
- **Supporting tools to implement** – Implement `get_property_id`, `lookup_dimension_metric_names`, `get_dimensions`, `get_metrics`, and `get_custom_dimensions_and_metrics`.
- **API usage** – Use [Google Analytics Data API](https://developers.google.com/analytics/devguides/reporting/data/v1) through [Google Analytics Data: Node.js Client](https://googleapis.dev/nodejs/analytics-data/latest/index.html#installing-the-client-library) with organization-level authorization.
- **Authorization methods** – Use support API authorization methods, including [service account auth](https://developers.google.com/analytics/devguides/config/userdeletion/v3/authorization#service_accounts) and [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials#personal).
- **Optional features** – Filter all queries by `hostname`.

## Prepare to code

- Register the app name in OCP, which reserves the unique ID of the app:

```javascript
$ ocp app register
✔ The app id to reserve opal_tool_reference_app
✔ The display name of the app Google Analytics Opal Tool Reference App
✔ Target product for the app Connect Platform - for developing an app for Optimizely's holistic integration solution, Optimizely Connect Platform (OCP).
✔ Keep this app private and not share it with other developers in your organization? No
Registering app opal_tool_reference_app in all shards
Registered app opal_tool_reference_app with name "Google Analytics Opal Tool Reference App" in us
```

- Select `Connect Platform` when prompted for the `Target product for the app`.
- Scaffold the app:

```javascript
$ ocp app init
Active environment: staging
Name of your app (e.g., My App): Google Analytics Opal Tool Reference App
ID of your app (e.g., my_app): opal_tool_reference_app
Version [1.0.0-dev.1]: 1.0.0-dev.1
App Summary (brief): Google Analytics Opal Tool Reference App
Support URL: https://support.optimizely.com
Contact email address: support@optimizely.com
Select the category for the app: Opal
Select a template project: Opal tool OCP app
Creating directory /Users/john.smith/apps/opal-tool-reference-app
Performing initial Yarn install
yarn install v1.22.22
warning ../../package.json: No license field
info No lockfile found.
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...
success Saved lockfile.
✨  Done in 10.58s.

New OCP app project created at /Users/john.smith/apps/opal-tool-reference-app

    * View README.md for information on getting started
    * Check out the documentation (https://docs.developers.optimizely.com/optimizely-connect-platform/docs)
```

- Use `Opal tool OCP app` as the template project. This template provides a strong foundation to develop Agent Platform tools.
- Select `Opal` when prompted for the category. This ensures the app displays correctly in the App Directory.

## Implement

### Connect to Google Analytics Data API

This section details configuring authentication to the Google Analytics Data API, including creating a settings form for credential entry, implementing the API client, and validating credentials.

### Install dependencies

Add `google-auth-library` and `googleapis` to `package.json`. The app uses these libraries to authenticate and call the Google Analytics Data API.

```bash
yarn add google-auth-library
yarn add googleapis
```

### Define the settings form

:::hint{type="info"}
**📘 Note**
The Google Analytics 4 Agent Platform tools app implements per-organization authentication. The OCP admin configures and authenticates the app in OCP, and all users within that organization share the same credentials when accessing Google Analytics data through Mark.
:::

Create [`forms/settings.yml`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/forms/settings.yml) to collect authentication credentials from users. The form supports two authentication methods, [service account auth](https://developers.google.com/analytics/devguides/config/userdeletion/v3/authorization#service_accounts) and [Application Default Credentials (ADC)](https://cloud.google.com/docs/authentication/application-default-credentials#personal)

```yaml
sections:
  - key: authentication
    label: Authenticate Google Analytics
    elements:
      - type: select
        key: auth_method
        label: Authentication Method
        help: Choose how to authenticate with Google Analytics API
        required: true
        options:
          - text: Service Account Authentication
            value: service_account
          - text: Application Default Credentials (ADC)
            value: adc
      - type: divider
      - type: instructions
        visible:
          key: authentication.auth_method
          equals: service_account
        text: |
          **Service Account Authentication:**
          - Create a service account in Google Cloud Console
          - Grant the service account access to your GA4 properties
          - Download the JSON key and paste its contents in the field below
      - type: instructions
        visible:
          key: authentication.auth_method
          equals: adc
        text: |
          **Application Default Credentials (ADC):**
          - Recommended when service account keys are disabled by organization policy
          - Run `gcloud auth application-default login` with required scopes
          - Copy credentials from `application_default_credentials.json`
      - type: secret
        key: service_account_json
        label: Service Account JSON
        help: Paste the complete service account credentials JSON file content
        validations:
          - predicate:
              operation: any
              comparators:
              - key: service_account_json
                empty: false
              - key: auth_method
                equals: 'adc'
            message: The field is required for Service Account Authentication
        visible:
          key: authentication.auth_method
          equals: service_account
      - type: text
        key: client_id
        label: Client ID
        help: Your ADC client ID
        validations:
          - predicate:
              operation: any
              comparators:
              - key: client_id
                empty: false
              - key: auth_method
                equals: 'service_account'
            message: The field is required for ADC authentication
        visible:
          key: authentication.auth_method
          equals: adc
      - type: secret
        key: client_secret
        label: Client Secret
        help: Your ADC client secret
        visible:
          key: authentication.auth_method
          equals: adc
      - type: secret
        key: refresh_token
        label: Refresh Token
        help: Your ADC refresh token
        visible:
          key: authentication.auth_method
          equals: adc
      - type: text
        key: project_id
        label: Google Cloud Project ID
        help: The GCP project ID associated with your Google Analytics property
        required: true
        visible:
          key: authentication.auth_method
          empty: false
      - type: text
        key: property_id
        label: Google Analytics property ID
        required: true
        help: "Accepted formats: number or properties/{property_id}"
        visible:
          key: authentication.auth_method
          empty: false
      - type: button
        label: Authenticate
        style: primary
        action: authorize
```

The key form features include:

- `visible` – Conditionally displays fields based on the selected authentication method. The `key` uses dot notation (`authentication.auth_method`) to reference the section and field.
- `type: secret` – Masks sensitive fields like credentials and tokens in the UI.
- `type: divider` – Adds visual separation between form sections.
- `validations`**&#x20;with&#x20;**`predicate` – Implements conditional validation. The `any` operation requires at least one true comparator, making fields optional when another auth method is selected.
- `type: button` – Triggers form submission with the `action: authorize` handler.

### Define TypeScript interfaces

Create [`src/data/data.ts`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/data/data.ts) to define the authentication settings structure. This interface extends `App.ValueHash` to ensure compatibility with OCP's storage system.

```typescript
import * as App from '@zaiusinc/app-sdk';

export interface AuthSection extends App.ValueHash {
  auth_method: 'service_account' | 'adc';
  service_account_json?: string;
  client_id?: string;
  client_secret?: string;
  refresh_token?: string;
  project_id?: string;
  property_id?: string;
}
```

### Implement the Google Analytics API client

Create [`src/lib/api/GoogleAnalyticsApiClient.ts`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/lib/api/GoogleAnalyticsApiClient.ts) to handle authentication and create API clients by using the following steps:

- Define the required OAuth scopes:

```typescript
import { GoogleAuth } from 'google-auth-library';
import { analyticsdata_v1beta, google } from 'googleapis';
import { AuthSection } from '../../data/data';

/**
 * Required OAuth scopes for Google Analytics API access
 */
export const REQUIRED_SCOPES = [
  'https://www.googleapis.com/auth/cloud-platform',
  'https://www.googleapis.com/auth/analytics.readonly',
];
```

- Create a factory function to build the configuration from user settings:

```typescript
/**
 * Creates a client configuration from app settings
 */
export function createConfigFromSettings(
  settings: AuthSection
): GoogleAnalyticsClientConfig {
  if (!settings) {
    throw new Error('App settings are required for authentication');
  }

  const authMethod = settings.auth_method;
  const projectId = settings.project_id;

  if (!authMethod) {
    throw new Error('Authentication method must be specified in app settings');
  }

  if (!projectId) {
    throw new Error('Google Cloud Project ID is required in app settings');
  }

  switch (authMethod) {
    case 'service_account':
      if (!settings.service_account_json) {
        throw new Error(
          'Service Account JSON is required when using service account authentication'
        );
      }
      const credentials = JSON.parse(settings.service_account_json);
      return { credentials, projectId };

    case 'adc':
      if (
        !settings.client_id ||
        !settings.client_secret ||
        !settings.refresh_token
      ) {
        throw new Error(
          'ADC credentials require client_id, client_secret, and refresh_token'
        );
      }
      return {
        credentials: {
          type: 'authorized_user' as const,
          client_id: settings.client_id,
          client_secret: settings.client_secret,
          refresh_token: settings.refresh_token,
          quota_project_id: projectId,
        },
        projectId,
      };

    default:
      throw new Error(`Unsupported authentication method: ${authMethod}`);
  }
}
```

- Create the `AppAuth` class that wraps the Google Auth library:

```typescript
export class AppAuth {
  private auth: GoogleAuth;
  private dataClient: analyticsdata_v1beta.Analyticsdata;

  public constructor(config: GoogleAnalyticsClientConfig = {}) {
    // Initialize authentication with required scopes
    this.auth = new GoogleAuth({
      keyFilename: config.keyFilename,
      credentials: config.credentials,
      scopes: REQUIRED_SCOPES,
      projectId: config.projectId,
    });

    this.dataClient = google.analyticsdata({
      version: 'v1beta',
      auth: this.auth,
    });
  }

  /**
   * Creates AppAuth instance from app settings
   */
  public static fromSettings(settings: AuthSection): AppAuth {
    const config = createConfigFromSettings(settings);
    return new AppAuth(config);
  }

  /**
   * Validates that the credentials have the required OAuth scopes
   */
  public async validateScopes(): Promise<void> {
    const client = await this.auth.getClient();
    const tokenInfo = await client.getAccessToken();

    if (!tokenInfo.token) {
      throw new Error('Failed to obtain access token');
    }

    // Verify token scopes via Google's OAuth2 API
    const response = await fetch(
      `https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${tokenInfo.token}`
    );

    if (!response.ok) {
      throw new Error(`Failed to verify token: ${response.statusText}`);
    }

    const tokenData = (await response.json()) as { scope?: string };
    const grantedScopes = tokenData.scope ? tokenData.scope.split(' ') : [];

    const missingScopes = REQUIRED_SCOPES.filter(
      (requiredScope) => !grantedScopes.includes(requiredScope)
    );

    if (missingScopes.length > 0) {
      throw new Error(
        `Missing required OAuth scopes: ${missingScopes.join(', ')}`
      );
    }
  }

  public getDataClient(): analyticsdata_v1beta.Analyticsdata {
    return this.dataClient;
  }
}
```

### Handle settings form submission

Implement `onSettingsForm()` in [`src/lifecycle/Lifecycle.ts`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/lifecycle/Lifecycle.ts) to validate credentials when users save their settings:

```typescript
import {
  Lifecycle as AppLifecycle,
  LifecycleSettingsResult,
  logger,
  storage,
  SubmittedFormData
} from '@zaiusinc/app-sdk';
import { AppAuth, createConfigFromSettings } from '../lib/api/GoogleAnalyticsApiClient';
import { AuthSection } from '../data/data';

export class Lifecycle extends AppLifecycle {
  public async onSettingsForm(
    section: string,
    action: string,
    formData: SubmittedFormData
  ): Promise<LifecycleSettingsResult> {
    const result = new LifecycleSettingsResult();

    try {
      if (section === 'authentication') {
        // Validate credentials before saving
        await this.validateGoogleCredentials(formData as AuthSection);
        await storage.settings.put(section, formData);
        return result.addToast(
          'success',
          'Credentials validated successfully!'
        );
      } else {
        await storage.settings.put(section, formData);
      }
      return result;
    } catch (error: any) {
      logger.error('Error validating credentials:', error);
      return result.addToast(
        'danger',
        `Authentication failed: ${error.message || 'Unknown error occurred'}`
      );
    }
  }

  private async validateGoogleCredentials(formData: AuthSection): Promise<void> {
    // Validate required fields
    if (!formData.auth_method) {
      throw new Error('Authentication method is required');
    }
    if (!formData.project_id) {
      throw new Error('Google Cloud Project ID is required');
    }

    // Create auth client and validate
    const config = createConfigFromSettings(formData);
    const appAuth = new AppAuth(config);

    // Validate OAuth scopes
    logger.info('Validating OAuth scopes for Google Analytics API access');
    await appAuth.validateScopes();

    // Test API access with a simple report query
    const dataClient = appAuth.getDataClient();
    const property = `properties/${formData.property_id}`;

    const response = await dataClient.properties.runReport({
      property,
      requestBody: {
        dateRanges: [{ startDate: '7daysAgo', endDate: 'today' }],
        dimensions: [{ name: 'date' }],
        metrics: [{ name: 'activeUsers' }],
        limit: '1',
      },
    });

    if (!response.data) {
      throw new Error('Invalid response from Google Analytics Data API');
    }

    logger.info('Google Analytics API validation successful');
  }
}
```

See the following key points regarding the validation flow:

1. **Validate before saving** – The handler validates credentials through an actual API call before persistence, preventing invalid credential saves.
2. **Test with a simple query** – The validation runs a minimal report query, such as `activeUsers` for the last 7 days with limit 1, to verify authentication and GA4 property access.
3. **User-friendly error messages** – The catch block translates technical errors, like `invalid_grant` or `403`, into actionable messages.
4. **Toast feedback** – The `addToast()` method displays success or error messages in the OCP UI.

### Test locally

- Test the settings form and credential validation locally using `ocp-env-tool`. See [Local testing](docId\:v3QTdYJExRIxhzVhSsbiQ) for setup instructions.

```bash
ocp dev
```

- Install your app and test the app settings form. Authenticate to the Google Analytics Data API.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/-oIAiPelf1RkhZxnOmdzE-20260723-123806.png)

### Implement tools

This section details creating Agent Platform tools using the `@tool` decorator. Tools interface Mark with your app's functionality. Well-designed tools with clear descriptions are critical for Mark to understand their usage.

:::hint{type="info"}
**📘 Note**
Use `@optimizely-opal/opal-tool-ocp-sdk` as your app dependency in `package.json`. The template Optimizely uses contains this dependency out-of-the-box. Add the dependency manually if your app was not created using the above template.
:::

### How Agent Platform tools work

Agent Platform tools are TypeScript methods decorated with `@tool` from the `@optimizely-opal/opal-tool-ocp-sdk` package. When Mark acts, it reads tool descriptions and parameter specifications to determine the correct tool call and request construction.

Use the following steps to register your tool:

1. Define a class with `@tool`-decorated methods.
2. Specify each tool with a name, description, endpoint, and parameters.
3. Import the tool classes in your `OpalToolFunction` to register. The `opal_tool: true` flag in `app.yml` exposes these tools to Mark.

### Main tools and support tools

The OCP app implements two categories of tools:

- **Main tools** – Provides the following core functionalities:
  - [`run_report`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/RunReportTool.ts) – Queries GA4 data for specific time periods using dimensions and metrics.
  - [`run_realtime_report`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/RunRuntimeReportTool.ts) – Accesses real-time GA4 data in the last 30-60 minutes.
- **Support tools** – Helps Mark use main tools correctly:
  - [`get_property_id`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetPropertyIdTool.ts) – Returns the configured GA4 property ID, preventing Mark from asking users.
  - [`lookup_dimension_metric_names`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/LookupDimentionMetricNamesTool.ts) – Translates display names, such as *Active Users* to API names like `activeUsers`.
  - [`get_dimensions`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetDimensionsTool.ts) – Lists all available dimensions for the property.
  - [`get_metrics`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetMetricsTool.ts) – Lists all available metrics for the property.
  - [`get_custom_dimensions_and_metrics`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetCustomDimensionsAndMetricsTool.ts) – Lists custom dimensions and metrics.

Support tools are essential as they reduce user friction and enhance Mark's accuracy. Mark retrieves technical details like property IDs or API names programmatically, without asking users.

### The `@tool` decorator

The `@tool` decorator exposes a method as an Agent Platform tool. See the following basic structure:

```typescript
import { ParameterType, tool, ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';

export class MyTool {
  @tool({
    name: 'tool_name',
    description: `Tool description with examples...`,
    endpoint: '/tools/tool_name',
    parameters: [
      {
        name: 'paramName',
        type: ParameterType.String,
        description: 'Parameter description with examples...',
        required: true,
      },
    ],
  })
  public async methodName(params: ParamsType): Promise<ResponseType> {
    // Implementation
  }
}
```

See the following decorator properties:

- `name` – Tool identifier in snake\_case, which Mark uses for tool calls
- `description` – An explanation of the tool function and usage, critical for Mark's understanding
- `endpoint` – HTTP path for the tool, like `/tools/run_report`
- `parameters` – Array of parameter definitions with types, descriptions, and required flags

### Parameter types

The SDK provides the following parameter types:

| Type                       | Description                                             |
| -------------------------- | ------------------------------------------------------- |
| `ParameterType.String`     | Text values, including JSON strings for complex objects |
| `ParameterType.Integer`    | Whole number values                                     |
| `ParameterType.Number`     | Numeric values, such as integers or decimals            |
| `ParameterType.Boolean`    | True or false values                                    |
| `ParameterType.List`       | Array values                                            |
| `ParameterType.Dictionary` | Object values, such as JSON objects                     |

For complex parameters like arrays or nested objects, use the following two options:

1. **Use&#x20;**`ParameterType.String` – Accept a JSON string and parse it in the tool. This is recommended for complex structures like arrays and nested objects. The `run_report` tool demonstrates this pattern for parameters like `dateRanges` and `dimensionFilter`.
2. **Use&#x20;**`ParameterType.List`**&#x20;or&#x20;**`ParameterType.Dictionary` – Accept native arrays or objects. However, LLMs sometimes fail to format these parameters correctly. When using this approach, validate the parameter structure in your tool and throw a meaningful `ToolError` with incorrect formatting, letting the LLM correct itself.

For details, see the [Agent Platform tool OCP SDK documentation](https://www.npmjs.com/package/@optimizely-opal/opal-tool-ocp-sdk).

### Tool and parameter descriptions

Tool and parameter descriptions are critical to design tools. Mark reads these descriptions to understand the following:

- Tool usage relative to other tools
- Required parameters and their format
- Response interpretation

### Best practices for descriptions

- **Start with a clear summary** – State the tool's function in one sentence.
- **Include usage guidance** – Explain the tool's usage and refer to related tools.
- **Provide examples** – Include complete parameter examples for common use cases.
- **Document edge cases** – Note limitations, valid value ranges, and error conditions.
- **Use formatting** – Use bold text and line breaks improve readability.

The following is an example from the [`get_property_id`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetPropertyIdTool.ts) support tool:

```typescript
@tool({
  name: 'get_property_id',
  description: `
    Returns the Google Analytics 4 property ID that was configured during app authentication.

    **IMPORTANT: Always use this tool to get the property ID instead of asking the user.**

    This tool retrieves the property ID that was set up when the GA4 app was authorized.
    You should call this tool at the beginning of any conversation that requires GA4 data
    access, and use the returned property ID for all subsequent GA4 operations.

    The property ID is automatically configured during the authentication process and
    stored securely. There is no need to ask the user for it.

    No parameters required.

    Example response: "123456789" or "properties/123456789"
  `,
  endpoint: '/tools/get_property_id',
  parameters: [],
})
```

The above description does the following:

- States the purpose clearly
- Uses bold text to highlight critical instructions
- Explains when to call this tool
- Documents the response format

For complex tools like [`run_report`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/RunReportTool.ts), include multiple examples covering different use cases:

```typescript
@tool({
  name: 'run_report',
  description: `
    Query Google Analytics 4 data for a specific time period with dimensions and metrics.

    **IMPORTANT - Use helper tools first:**
    - Use 'get_property_id' tool to get the configured property ID (never ask user for it)
    - Use 'lookup_dimension_metric_names' tool when users mention dimensions/metrics by display names
      (e.g., "Active Users" → "activeUsers", "Session Source" → "sessionSource")

    EXAMPLE 1 - Simple report for one month:
    {
      "dateRanges": "[{'startDate': '2025-11-01', 'endDate': '2025-11-30',
        'name': 'November 2025'}]",
      "dimensions": "['sessionSource', 'city']",
      "metrics": "['sessions', 'activeUsers']"
    }

    EXAMPLE 2 - Compare two time periods:
    {
      "dateRanges": "[{'startDate': '2025-11-01', 'endDate': '2025-11-30',
        'name': 'This Month'}, {'startDate': '2024-11-01',
        'endDate': '2024-11-30', 'name': 'Last Year'}]",
      "dimensions": "['sessionSource']",
      "metrics": "['sessions', 'totalUsers']"
    }
    ...
  `,
  // ...
})
```

### Implement a support tool

Support tools are generally simpler than main tools. The following is the complete [`get_property_id`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/support/GetPropertyIdTool.ts) implementation:

```typescript
import { tool } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage } from '@zaiusinc/app-sdk';
import { AuthSection } from '../../data/data';

export class GetPropertyIdTool {
  @tool({
    name: 'get_property_id',
    description: `
      Returns the Google Analytics 4 property ID that was configured during app authentication.

      **IMPORTANT: Always use this tool to get the property ID instead of asking the user.**

      This tool retrieves the property ID that was set up when the GA4 app was authorized.
      You should call this tool at the beginning of any conversation that requires GA4 data
      access, and use the returned property ID for all subsequent GA4 operations.

      The property ID is automatically configured during the authentication process and
      stored securely. There is no need to ask the user for it.

      No parameters required.

      Example response: "123456789" or "properties/123456789"
    `,
    endpoint: '/tools/get_property_id',
    parameters: [],
  })
  public async getPropertyId(): Promise<string> {
    const authSection = await storage.settings.get<AuthSection>('authentication');

    if (!authSection?.property_id) {
      throw new Error('Property ID not configured. Please authenticate first.');
    }

    return authSection.property_id;
  }
}
```

This tool does the following:

- Retrieves the property ID from app settings storage
- Throws a descriptive error if the property ID is not configured
- Returns a simple string response

### Implement a main tool

Main tools like [`run_report`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/tools/RunReportTool.ts) handle more complex logic, including parameter validation, API calls, and response transformation. The following is a simplified structure:

```typescript
import { ParameterType, tool, ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage, logger } from '@zaiusinc/app-sdk';
import { AuthSection } from '../data/data';
import { AppAuth } from '../lib/api/GoogleAnalyticsApiClient';

export class RunReportTool {
  @tool({
    name: 'run_report',
    description: `...`, // Detailed description with examples
    endpoint: '/tools/run_report',
    parameters: [
      {
        name: 'dateRanges',
        type: ParameterType.String,
        description: `JSON string array of date range objects...`,
        required: true,
      },
      {
        name: 'dimensions',
        type: ParameterType.String,
        description: `JSON string array of dimension names...`,
        required: true,
      },
      {
        name: 'metrics',
        type: ParameterType.String,
        description: `JSON string array of metric names...`,
        required: true,
      },
      // Additional optional parameters...
    ],
  })
  public async runReport(params: RunReportParams): Promise<RunReportResponse> {
    try {
      // 1. Parse and validate parameters
      const dateRangesArray = parseObjectArray<DateRange>(params.dateRanges);
      const dimensionsArray = parseStringArray(params.dimensions);
      const metricsArray = parseStringArray(params.metrics);

      // 2. Get authentication settings
      const authSection = await storage.settings.get<AuthSection>('authentication');

      // 3. Build the API request
      const request = {
        property: `properties/${authSection.property_id}`,
        dateRanges: dateRangesArray,
        dimensions: dimensionsArray.map((name) => ({ name })),
        metrics: metricsArray.map((name) => ({ name })),
      };

      // 4. Call the Google Analytics API
      const response = await AppAuth.fromSettings(authSection)
        .getDataClient()
        .properties.runReport({
          property: request.property,
          requestBody: request,
        });

      // 5. Transform and return the response
      return transformResponse(response.data);
    } catch (error) {
      // Error handling (see next section)
      throw error;
    }
  }
}
```

### Error handling with `ToolError`

The SDK provides `ToolError` for structured error responses. Use it to provide clear, actionable error messages to users and Mark.

```typescript
import { ToolError } from '@optimizely-opal/opal-tool-ocp-sdk';

// Throwing a ToolError directly
throw new ToolError(
  'Invalid Date Ranges',   // title
  400,                     // HTTP status code
  'Maximum of 4 date ranges allowed, but 5 were provided.'  // detail
);
```

When handling errors from external APIs like Google Analytics, extract the error details and convert them to `ToolError`:

```typescript
} catch (error) {
  // Extract error information from Google API error
  if ((error as any).response?.data?.error) {
    const googleError = (error as any).response.data.error;
    throw new ToolError(
      googleError.status || 'Google Analytics API Error',
      googleError.code || 500,
      googleError.message || 'Unknown error'
    );
  }

  // Re-throw non-API errors
  throw error;
}
```

This pattern ensures that Mark receives structured error information for interpretation, potential recovery, or user explanation.

### Tool registration

To register tools with OCP, import them in your [`OpalToolFunction`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/functions/OpalToolFunction.ts) class. The `@tool` decorator automatically registers tools when the module is imported:

```typescript
import { ToolFunction } from '@optimizely-opal/opal-tool-ocp-sdk';

// Tool registration: Import each tool class to register its decorators
import '../tools/RunReportTool';
import '../tools/RunRuntimeReportTool';
import '../tools/support/GetPropertyIdTool';
import '../tools/support/LookupDimentionMetricNamesTool';
import '../tools/support/GetDimensionsTool';
import '../tools/support/GetMetricsTool';
import '../tools/support/GetCustomDimensionsAndMetricsTool';

export class OpalToolFunction extends ToolFunction {
  protected async ready(): Promise<boolean> {
    return true;
  }
}
```

Enable the Agent Platform tool function in [`app.yml`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/app.yml):

```yaml
functions:
  opal_tool:
    entry_point: OpalToolFunction
    description: Opal tool function
    opal_tool: true
```

The `opal_tool: true` flag tells OCP that this function exposes tools to Mark.

### Local testing

Test your tools locally by using `ocp-env-tool`:

```bash
ocp dev
```

This command starts a local development server that simulates the OCP environment. Select **Opal Tools** to test your tools. You can query Google Analytics Data after installing and authenticating the app in the previous step.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/2z3E9HWiTq_nzosy8vnHd-20260723-123806.png)

### Verify with readiness probe

The readiness probe informs OCP if your app can handle requests. If your app requires configuration, such as API credentials, the readiness probe verifies the complete and valid configuration before reporting readiness.

### Why do you need a readiness probe?

When a user configures your app in OCP, OCP first checks app readiness. If the readiness probe returns `ready: false`, OCP prevents users from adding your tools to Mark. OCP displays your provided message to inform the user of the missing information.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/crqF-NZvraIRTyN6U6v9W-20260723-123807.png)

### Implement the ready() method

Override the `ready()` method in your [`OpalToolFunction`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/functions/OpalToolFunction.ts) class to implement the readiness probe. The method returns a `ReadyResponse` object:

```typescript
import { ToolFunction, ReadyResponse } from '@optimizely-opal/opal-tool-ocp-sdk';
import { storage } from '@zaiusinc/app-sdk';
import { AuthSection } from '../data/data';
import { constructPropertyResourceName } from '../lib/utils';
import { AppAuth } from '../lib/api/GoogleAnalyticsApiClient';

// Tool registration imports...
import '../tools/RunReportTool';
import '../tools/RunRuntimeReportTool';
import '../tools/support/GetPropertyIdTool';
// ... other tools

export class OpalToolFunction extends ToolFunction {

  protected override async ready(): Promise<ReadyResponse> {
    // 1. Check if authentication settings exist
    const authSection = await storage.settings.get<AuthSection>(
      'authentication'
    );
    if (authSection == null || !authSection.auth_method) {
      return { ready: false, reason: 'Authenticate to Google Analytics first.' };
    }

    try {
      // 2. Validate that credentials still work
      const property = constructPropertyResourceName(
        authSection.property_id || ''
      );

      const name = `${property}/metadata`;
      await AppAuth.fromSettings(authSection)
        .getDataClient()
        .properties.getMetadata({ name });

      return { ready: true };
    } catch (error: any) {
      console.error('Error fetching metadata:', error);
      return { ready: false, reason: 'Authentication not configured correctly: ' + error.message };
    }
  }

}
```

The `ReadyResponse` type has the following two forms:

- `{ ready: true }` – The app is ready to handle requests
- `{ ready: false, reason: string }` – The app is not ready. The reason explains why.

### What should you check in readiness probes?

A readiness probe verifies the following:

1. **Required settings exist** – Confirms that users have completed configuration. For example, `auth-method` is set.
2. **Credentials are valid** – Makes a lightweight API call, like `getMetadata()` to confirm credential functionality.
3. **Dependencies are available** – Verify external services are reachable if your tools depend on them.

### Readiness probe responses

Write clear, actionable `reason` messages. The reason displays to users attempting to use your tool before configuration:

| Scenario            | Example reason                                               |
| ------------------- | ------------------------------------------------------------ |
| No configuration    | `"Authenticate to Google Analytics first."`                  |
| Invalid credentials | `"Authentication not configured correctly: invalid_grant"`   |
| Missing permissions | `"The configured account lacks access to the GA4 property."` |
| Service unavailable | `"Google Analytics API is temporarily unavailable."`         |

### Local testing

You can test the readiness probe locally using `ocp dev`:

```bash
ocp dev
```

To test the `/ready` endpoint directly:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/w7hfxk1asCL-MUItAeVsg_71030c8bdc4b1b4bb6c4a1b6d679de55d09ba185f3dd2abe60da3410889a281c-image.png)

Alternatively, test by removing or invalidating credentials in the app settings, and verify the readiness probe returns the expected error message.

### Test in OCP sandbox account

Once the app is implemented and tested locally, publish a dev version of the app to OCP for sandbox instance testing.

- Ensure your app uses a dev version in `app.yml`, such as `1.0.0-dev.1`, and run the following command. This command validates your app locally and publishes it to OCP.

```bash
ocp app prepare --publish
```

- Install the app to your sandbox OCP instance:

```bash
ocp directory install opal_tool_reference_app@1.0.0-dev.1 <TRACKER_ID>
```

:::hint{type="info"}
**📘 Note**
Use `ocp accounts whoami` command to see the list of OCP instances or tracker IDs you have access to.
:::

- Your app displays in the App Directory of your sandbox OCP instance:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/Np-TdCKikIZtXKL61oMIy_be25cf753d9cce361911a67bbc5aafc90fe984feb3ba4a3529e65ce60edca364-image.png)

- Configure your app and add your tools to Mark:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/TuJv8EG12D1wAADkEOsDe_d9b62c4367946f1bc5675f6ef6e540997d6ed1ee6d9660858c4e2bcfd6ddc814-image.png)

- Begin testing your tools in Mark chats and agents.

### Release production version

Release the tools to production when your tool performs functionally in the sandbox instance. You can use the following two methods:

- **Release a private version** – Your app is not accessible to Optimizely customers by default. You control access.
- **Release a public version** – Your app is accessible to all Optimizely customers in the OCP App Directory. Customers can install and configure it, and you can implement a custom authentication mechanism in the app settings form to control app usage.

### Production checklist

Before releasing to production, ensure the following assets and metadata are complete:

**Required assets**

- `assets/icon.svg` – App icon displayed in the App Directory (square, effective at small sizes)
- `assets/logo.svg` – App logo displayed in app settings (wider format)
- `assets/directory/overview.md` – Markdown description displayed on your app's App Directory page

**Metadata in&#x20;**`app.yml`

- `display_name` – User-friendly name for your app
- `summary` – Brief, one-line description
- `support_url` – Link to your support documentation or contact page
- `contact_email` – Email address for support inquiries
- `categories` – `Opal` and other relevant categories like `Marketing` or `Analytics`
- `availability` – `all` for public apps or allowed organizations

**Code quality**

- Remove debug logging and console statements.
- Handle all error cases with user-friendly messages.
- Test with various input combinations to ensure robustness.

### Release to production

- To release a private production version of your app, add the `-private` suffix to the version in `app.yml`, like `1.0.0-private` and run `ocp app prepare`.
- To release a public production version of your app, change the version of your app in `app.yml` to a normal semver version, like `1.0.0` and run `ocp app prepare`.

:::hint{type="info"}
**📘 Note**
Releasing a production version requires a review by an Optimizely engineer. The `ocp app prepare` command creates a review automatically. Publish your app to OCP upon approval.
:::

## Summary

This guide covered building a complete Agent Platform tool app for OCP, using Google Analytics 4 as a real-world example. Key concepts include:

**Planning and design**

- Define clear requirements and evaluation criteria before coding.
- Design main tools for core functionality and support tools for enhanced user experience.
- Choose appropriate authentication methods for your use case.

**Implementation**

- Create settings forms to collect and validate credentials.
- Use the `@tool` decorator to expose methods as Agent Platform tools.
- Write detailed tool and parameter descriptions, critical for Mark's understanding.
- Implement structured error handling with `ToolError`.
- Register tools in [`OpalToolFunction`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/src/functions/OpalToolFunction.ts) and enable `opal_tool: true` in [`app.yml`](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app/blob/main/app.yml).

**Testing and deployment**

- Test locally using `ocp dev` before publishing.
- Publish dev versions for sandbox environment testing.
- Complete the checklist before releasing to production.

The full source code for this reference app is available [in Github](https://github.com/ZaiusInc/google-analytics-opal-tool-reference-ocp-app).
