> ## Documentation Index
> Fetch the complete documentation index at: https://docs.coreflux.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Looker

> Client-mode REST API connector for Looker (Google Cloud core and original Looker): run inline queries and saved Looks and publish the rows on MQTT, trigger scheduled plans when a shift closes, render dashboards to PDF or PNG, and expire datagroup caches, authenticated with an API key login

Technical Preview

## Overview

A client-mode connector for the [Looker API 4.0](https://docs.cloud.google.com/looker/docs/reference/looker-api/latest). Use it to put Looker results on MQTT, such as OEE per line, scrap by shift or open maintenance orders for an andon board or HMI, and to let plant events drive Looker: run a scheduled plan once when a shift or batch closes, render a dashboard to PDF, or expire a datagroup cache after new data lands in the warehouse.

The connector `LookerRestApi` in `LookerRestApi.lot` has 44 events (30 GET, 11 POST, 2 PATCH, 1 PUT). Five are writes: `ScheduledPlanRunOnceById`, `ScheduledPlanRunOnce`, `ScheduledPlanCreate`, `ScheduledPlanUpdate` and `DatagroupUpdate`. There are no DELETE events.

Set `BASE_ADDRESS` to your Looker URL with no trailing slash and no `/api`, for example `https://acme.cloud.looker.com` or a Looker (Google Cloud core) instance URL ending in `looker.app`. Every endpoint already starts with `/api/4.0`. Some older self-hosted instances serve the API on port 19999 (`https://acme.looker.com:19999`). Topics use `analytics/looker/<object>/<verb>`; replies land on `<topic>/response`.

See the base guide: [REST API Connectors](/latest/lot-language/routes/rest-api-routes).

> [!NOTE]
>
> **Auth:** a Looker admin creates an API key (client ID and client secret) on a user in **Admin > Users > Edit Keys**. The `Login` event posts `client_id` and `client_secret` as a form to `/api/4.0/login` and gets back an `access_token` that lasts about an hour. Every other event except `Versions` sends `Authorization: token <access_token>` (the word `token` is required). The token acts as that user, so give the API user a role with only the models and permissions the broker needs. Keep `LOOKER_CLIENT_ID`, `LOOKER_CLIENT_SECRET` and the current token in broker secrets, read them with `GET SECRET` in the action that builds the payload, never retain topics that carry them, and restrict `analytics/looker/auth/#` with ACLs.

## Events

| Event | Request | Source topic | Notes |
| - | - | - | - |
| Versions | `GET /api/4.0/versions` | `analytics/looker/versions/get` | Smoke, no auth: API versions, Looker release and web\_server\_url. Payload: any JSON, for example an empty object. |
| Login | `POST /api/4.0/login` | `analytics/looker/auth/login` | Get an API access token (expires\_in about 3600 s). |
| Me | `GET /api/4.0/user?{value.json.query}` | `analytics/looker/user/me` | The API user behind the token. |
| Session | `GET /api/4.0/session` | `analytics/looker/session/get` | Current API session (workspace\_id: production or dev). |
| QueryRunInline | `POST /api/4.0/queries/run/{value.json.result_format}?{value.json.query}` | `analytics/looker/queries/run_inline` | Run an inline query and publish the result (catalog event). result\_format json, json\_detail, json\_bi, csv, txt, md, sql. |
| QueryCreate | `POST /api/4.0/queries?{value.json.query}` | `analytics/looker/queries/create` | Create an immutable query object (returns id and slug; reuse with QueryRun). |
| QueryGet | `GET /api/4.0/queries/{value.json.query_id}?{value.json.query}` | `analytics/looker/queries/get` | A saved query definition. |
| QueryRun | `GET /api/4.0/queries/{value.json.query_id}/run/{value.json.result_format}?{value.json.query}` | `analytics/looker/queries/run` | Run a saved query by id. result\_format json, csv, ... |
| QueryTaskCreate | `POST /api/4.0/query_tasks?{value.json.query}` | `analytics/looker/query_tasks/create` | Start an async query (long queries). |
| QueryTaskGet | `GET /api/4.0/query_tasks/{value.json.query_task_id}` | `analytics/looker/query_tasks/get` | Async query task status. |
| QueryTaskResults | `GET /api/4.0/query_tasks/multi_results?query_task_ids={value.json.query_task_ids}` | `analytics/looker/query_tasks/results` | Results of one or more async query tasks. query\_task\_ids: comma-separated ids. |
| RunningQueriesList | `GET /api/4.0/running_queries` | `analytics/looker/running_queries/list` | Queries running now (admin / see\_queries). |
| LooksSearch | `GET /api/4.0/looks/search?{value.json.query}` | `analytics/looker/looks/search` | Search Looks (saved reports). |
| LookGet | `GET /api/4.0/looks/{value.json.look_id}?{value.json.query}` | `analytics/looker/looks/get` | A Look with its query. |
| LookRun | `GET /api/4.0/looks/{value.json.look_id}/run/{value.json.result_format}?{value.json.query}` | `analytics/looker/looks/run` | Run a Look and publish its rows (catalog event). result\_format json, csv, txt, ... |
| DashboardsSearch | `GET /api/4.0/dashboards/search?{value.json.query}` | `analytics/looker/dashboards/search` | Search dashboards. |
| DashboardGet | `GET /api/4.0/dashboards/{value.json.dashboard_id}?{value.json.query}` | `analytics/looker/dashboards/get` | A dashboard (tiles, filters). |
| DashboardElements | `GET /api/4.0/dashboards/{value.json.dashboard_id}/dashboard_elements?{value.json.query}` | `analytics/looker/dashboards/elements` | Dashboard tiles with their query ids (run them with QueryRun). |
| ScheduledPlansList | `GET /api/4.0/scheduled_plans?{value.json.query}` | `analytics/looker/scheduled_plans/list` | Scheduled plans. |
| ScheduledPlanGet | `GET /api/4.0/scheduled_plans/{value.json.scheduled_plan_id}?{value.json.query}` | `analytics/looker/scheduled_plans/get` | One scheduled plan. |
| ScheduledPlansForLook | `GET /api/4.0/scheduled_plans/look/{value.json.look_id}?{value.json.query}` | `analytics/looker/scheduled_plans/for_look` | Scheduled plans of a Look. |
| ScheduledPlansForDashboard | `GET /api/4.0/scheduled_plans/dashboard/{value.json.dashboard_id}?{value.json.query}` | `analytics/looker/scheduled_plans/for_dashboard` | Scheduled plans of a dashboard. |
| ScheduledPlanRunOnceById | `POST /api/4.0/scheduled_plans/{value.json.scheduled_plan_id}/run_once` | `analytics/looker/scheduled_plans/run_once_by_id` | Write, sends deliveries. Run an existing schedule now, e.g. when a shift closes (catalog event). |
| ScheduledPlanRunOnce | `POST /api/4.0/scheduled_plans/run_once` | `analytics/looker/scheduled_plans/run_once` | Write, sends deliveries. Run an ad-hoc schedule now without saving it. |
| ScheduledPlanCreate | `POST /api/4.0/scheduled_plans` | `analytics/looker/scheduled_plans/create` | Write. Create a schedule. |
| ScheduledPlanUpdate | `PATCH /api/4.0/scheduled_plans/{value.json.scheduled_plan_id}` | `analytics/looker/scheduled_plans/update` | Write. Change or pause a schedule (no delete event: set enabled false). |
| LookmlModelsList | `GET /api/4.0/lookml_models?{value.json.query}` | `analytics/looker/models/list` | LookML models and their explores. |
| LookmlModelGet | `GET /api/4.0/lookml_models/{value.json.lookml_model_name}?{value.json.query}` | `analytics/looker/models/get` | One LookML model. |
| ExploreGet | `GET /api/4.0/lookml_models/{value.json.lookml_model_name}/explores/{value.json.explore_name}?{value.json.query}` | `analytics/looker/models/explore` | An explore with its dimensions and measures (field names for inline queries). |
| SqlQueryCreate | `POST /api/4.0/sql_queries` | `analytics/looker/sql_queries/create` | Create a SQL Runner query (returns slug). |
| SqlQueryRun | `POST /api/4.0/sql_queries/{value.json.slug}/run/{value.json.result_format}` | `analytics/looker/sql_queries/run` | Run a SQL Runner query by slug. result\_format json, json\_detail, csv, txt. |
| LookRenderTaskCreate | `POST /api/4.0/render_tasks/looks/{value.json.look_id}/{value.json.result_format}?width={value.json.width}&height={value.json.height}` | `analytics/looker/render_tasks/look` | Start rendering a Look as png or jpg (width / height in px). Poll RenderTaskGet, then RenderTaskResults. |
| DashboardRenderTaskCreate | `POST /api/4.0/render_tasks/dashboards/{value.json.dashboard_id}/{value.json.result_format}?width={value.json.width}&height={value.json.height}` | `analytics/looker/render_tasks/dashboard` | Start rendering a dashboard as pdf, png or jpg. |
| RenderTaskGet | `GET /api/4.0/render_tasks/{value.json.render_task_id}` | `analytics/looker/render_tasks/get` | Render task status (success when done). |
| RenderTaskResults | `GET /api/4.0/render_tasks/{value.json.render_task_id}/results` | `analytics/looker/render_tasks/results` | Rendered file (binary pdf / png / jpg). The broker publishes the body as received. |
| FoldersSearch | `GET /api/4.0/folders/search?{value.json.query}` | `analytics/looker/folders/search` | Search folders. |
| FolderLooks | `GET /api/4.0/folders/{value.json.folder_id}/looks?{value.json.query}` | `analytics/looker/folders/looks` | Looks in a folder. |
| FolderDashboards | `GET /api/4.0/folders/{value.json.folder_id}/dashboards?{value.json.query}` | `analytics/looker/folders/dashboards` | Dashboards in a folder. |
| AlertsSearch | `GET /api/4.0/alerts/search?{value.json.query}` | `analytics/looker/alerts/search` | Search alerts (threshold conditions on dashboard tiles). |
| AlertGet | `GET /api/4.0/alerts/{value.json.alert_id}` | `analytics/looker/alerts/get` | One alert (comparison\_type, threshold, destinations). |
| DatagroupsList | `GET /api/4.0/datagroups` | `analytics/looker/datagroups/list` | Datagroups (cache and PDT trigger state). |
| DatagroupUpdate | `PATCH /api/4.0/datagroups/{value.json.datagroup_id}` | `analytics/looker/datagroups/update` | Write. Expire the cache of a datagroup after new plant data lands, e.g. |
| ConnectionsList | `GET /api/4.0/connections?{value.json.query}` | `analytics/looker/connections/list` | Database connections. |
| ConnectionTest | `PUT /api/4.0/connections/{value.json.connection_name}/test?{value.json.query}` | `analytics/looker/connections/test` | Test a database connection (no change). |

Payload fields are JSON on the source topic: ids (`query_id`, `look_id`, `dashboard_id`, `scheduled_plan_id`, `query_task_id`, `render_task_id`, `folder_id`, `alert_id`, `datagroup_id`, `connection_name`, `lookml_model_name`, `explore_name`, `slug`), `result_format` (`json`, `json_detail`, `csv`, `txt`, `md`, `sql`, or `png` / `jpg` / `pdf` for renders), `query` (a URL query string without `?`, or `""`), `width` and `height` for renders, `body` (a JSON object) for POST and PATCH events, and `form` for `Login`.

## Basic Syntax

An excerpt of the template (the full file has all 44 events and comments):

```lot wrap theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
DEFINE ROUTE LookerRestApi WITH TYPE REST_API
    ADD REST_API_CONFIG
        WITH BASE_ADDRESS "https://YOUR_LOOKER_HOST"
        WITH ENABLE_CLIENT "true"
        WITH USE_SSL "true"
        WITH REQUEST_TIMEOUT '120'
        WITH DEFAULT_CONTENT_TYPE "application/json"
        # Smoke: GET /api/4.0/versions -> 200 (no auth). Auth: Authorization: token {value.json.access_token}. Never commit API keys.
    ADD EVENT Versions
        WITH SOURCE_TOPIC "analytics/looker/versions/get"
        WITH DESTINATION_TOPIC "analytics/looker/versions/get/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/4.0/versions"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
    ADD EVENT Login
        WITH SOURCE_TOPIC "analytics/looker/auth/login"
        WITH DESTINATION_TOPIC "analytics/looker/auth/login/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/4.0/login"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Content-Type" "application/x-www-form-urlencoded"
        WITH BODY "{value.json.form}"
    ADD EVENT QueryRunInline
        WITH SOURCE_TOPIC "analytics/looker/queries/run_inline"
        WITH DESTINATION_TOPIC "analytics/looker/queries/run_inline/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/4.0/queries/run/{value.json.result_format}?{value.json.query}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "token {value.json.access_token}"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
    ADD EVENT LookRun
        WITH SOURCE_TOPIC "analytics/looker/looks/run"
        WITH DESTINATION_TOPIC "analytics/looker/looks/run/response"
        WITH METHOD "GET"
        WITH ENDPOINT "/api/4.0/looks/{value.json.look_id}/run/{value.json.result_format}?{value.json.query}"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "token {value.json.access_token}"
    ADD EVENT ScheduledPlanRunOnceById
        WITH SOURCE_TOPIC "analytics/looker/scheduled_plans/run_once_by_id"
        WITH DESTINATION_TOPIC "analytics/looker/scheduled_plans/run_once_by_id/response"
        WITH METHOD "POST"
        WITH ENDPOINT "/api/4.0/scheduled_plans/{value.json.scheduled_plan_id}/run_once"
        WITH HEADERS
            ADD "Accept" "application/json"
            ADD "User-Agent" "Coreflux-Broker REST_API route (https://www.coreflux.org)"
            ADD "Authorization" "token {value.json.access_token}"
            ADD "Content-Type" "application/json"
        WITH BODY "{value.json.body}"
```

## Examples

Log in and keep the token in a secret-backed action (the response carries `access_token`):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"form": "client_id=<LOOKER_CLIENT_ID>&client_secret=<LOOKER_CLIENT_SECRET>"}
```

Publish to `analytics/looker/auth/login`. Then run an inline query and get the rows as JSON on `analytics/looker/queries/run_inline/response`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "result_format": "json", "query": "limit=100&apply_formatting=false&cache=true", "body": {"model": "plant_ops", "view": "oee", "fields": ["oee.line", "oee.availability", "oee.performance"], "filters": {"oee.date": "today"}, "sorts": ["oee.line"], "limit": "100", "query_timezone": "Europe/Lisbon"}}
```

Run a saved Look on `analytics/looker/looks/run`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "look_id": "42", "result_format": "json", "query": "limit=100&apply_formatting=true"}
```

Send a shift report now from an existing schedule on `analytics/looker/scheduled_plans/run_once_by_id` (this delivers email or other destinations):

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "scheduled_plan_id": "15", "body": {"scheduled_plan_destination": [{"type": "email", "format": "csv", "address": "shift-lead@example.com"}]}}
```

Expire a datagroup cache after a batch of plant data lands, on `analytics/looker/datagroups/update`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "datagroup_id": "11", "body": {"stale_before": 1791190800}}
```

Render a dashboard: publish to `analytics/looker/render_tasks/dashboard`, poll `analytics/looker/render_tasks/get` until `status` is `success`, then read the file from `analytics/looker/render_tasks/results/response`:

```json theme={"theme":"css-variables","languages":{"custom":["/languages/lot.json"]}}
{"access_token": "<access_token>", "dashboard_id": "7", "result_format": "pdf", "width": "1280", "height": "1600", "body": {"dashboard_style": "tiled", "dashboard_filters": "Line=Line 2"}}
```

## Notes

* Long queries: use `QueryTaskCreate` and poll `QueryTaskResults` instead of waiting on `QueryRunInline`; `RunningQueriesList` shows what is running.
* Field names for inline queries come from `ExploreGet` (dimensions and measures of an explore). Dashboard tiles carry a `query_id` (`DashboardElements`) that `QueryRun` can run directly.
* `SqlQueryCreate` and `SqlQueryRun` need the `use_sql_runner` permission. `RenderTaskResults` returns a binary file, published as received.
* Not packaged on purpose: every DELETE (Looks, dashboards, schedules, alerts, logout, killing queries; pause a schedule with `ScheduledPlanUpdate` and `{"enabled": false}` instead), user and content administration, LookML project and git operations, and embed SSO. A scheduled plan with a webhook destination can POST to a separate server-mode connector.
* Requests are limited per instance; keep polling intervals coarse and prefer `cache=true`.

## Status

Technical preview, blocked on a live test. Static, spec, example-body and mock checks pass. A live run needs a Looker instance URL and an API key (client ID and secret) for a user with access to at least one model.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.