# Overview

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2F8bD77iUcj1BTfv8Z0pwg%2FLinkedin%20Banner.png?alt=media&amp;token=7810db4c-1bcb-4a9e-88d9-aa7b4e7d4689" alt=""><figcaption></figcaption></figure>

**Autonomous testing that evolves with your codebase. No flaky tests, no maintenance burden.**

Duku explores your site, maps reachable flows, and replays critical journeys on every deploy.

{% hint style="success" %}
Start with one public URL. Duku handles discovery, graph building, and replay.
{% endhint %}

### Why teams choose Duku

* **No scripts to maintain:** Duku discovers flows automatically.
* **Real browser coverage:** It validates full journeys, not isolated functions.
* **CI-ready by default:** Run explorations and intent tests on every PR.
* **Finds blind spots:** It uncovers paths you probably did not test manually.

### How Duku works

{% stepper %}
{% step %}

#### Create a product

Point Duku at your site with a base URL.
{% endstep %}

{% step %}

#### Explore the application

Duku clicks links, submits forms, and records each reachable state.
{% endstep %}

{% step %}

#### Review the graph

Inspect pages, transitions, screenshots, and captured errors.
{% endstep %}

{% step %}

#### Replay critical flows

Run intent tests for journeys like login, signup, and checkout.
{% endstep %}
{% endstepper %}

### Read next

* [Getting Started](/getting-started/create-a-product)
* [Core Concepts](/core-concepts/products-and-targets)
* [Viewport Dashboard](/viewport-dashboard/overview)
* [Integrations](/integrations/choosing-your-integration)
* [Roles & Permissions](/roles-and-permissions/overview)


# Create a Product

Create the top-level container for your targets, explorations, and results.

A **product** is the top-level container for everything Duku tracks for an application.

It holds your targets, explorations, intent tests, and results. In most teams, one product maps to one app or site.

{% stepper %}
{% step %}

#### Open Products

Sign in to `https://app.duku.ai`, then open **Products**.
{% endstep %}

{% step %}

#### Create the product

Click **Create product**.
{% endstep %}

{% step %}

#### Fill in the fields

* **Name:** A clear label like `Marketing Site` or `Web App`
* **Description:** Optional context for your team
* **Base URL:** The general root URL for the product
  {% endstep %}

{% step %}

#### Save it

Click **Create product**.
{% endstep %}

{% step %}

#### Add a credential set if needed

If the app needs a login, open the product's settings and add a credential set there.
{% endstep %}

{% step %}

#### Create the first target

Open the product.

Use the target menu in the top-right navbar, next to the active product.

If you add credentials, set the product's Base URL so Duku reaches the sign-in page, for example `https://app.example.com/signin`.
{% endstep %}
{% endstepper %}

{% hint style="info" %}
Create separate products for separate applications. Explorations start from the product's base URL. Targets record which version you tested; CI can override the URL for a single run.
{% endhint %}


# Run Your First Exploration

Start an exploration and understand what Duku captures.

Once your product exists and a target is available, you can start an exploration to discover reachable pages and user flows.

{% stepper %}
{% step %}

#### Open the product and choose a target

Open the product and choose a target from the target menu in the top-right navbar, or create one from the same menu if none exists yet.
{% endstep %}

{% step %}

#### Start exploration

Click **Start exploration**.
{% endstep %}

{% step %}

#### Watch progress

Open the **Exploration** tab.

Scroll to the runs table to follow progress in real time.
{% endstep %}
{% endstepper %}

### What Duku captures

During exploration, Duku:

1. Visits your application's URL
2. Finds interactive elements like links, buttons, forms, and menus
3. Executes actions in a real browser
4. Records screenshots and page state at each transition
5. Captures errors, network failures, and broken elements
6. Builds a graph of discovered states and transitions

{% hint style="info" %}
If the app needs a login, add a credential set in the product's settings and set the product's Base URL so Duku reaches the sign-in page.
{% endhint %}


# Review Results

Review the state graph, errors, and screenshots from an exploration.

Once an exploration completes, open the product's **Exploration** tab and scroll to the runs table.

Open a run there to inspect what Duku found.

### What to review

#### State graph

The graph shows every discovered page and how states connect. Nodes are states. Edges are actions like clicks, form submissions, and navigations.

#### Run replay

Each run includes a live recording of the full browser session.

Use it to watch every step Duku took, see where a flow changed, and confirm what happened before an error appeared.

#### Errors

Review:

* Runtime errors
* Network request failures
* Broken interactive elements
* Console errors and warnings

Each error includes the page where it occurred and a screenshot from that state.

#### Screenshots

Every state transition includes a full-page screenshot. Use them to verify what Duku actually saw.

{% hint style="info" %}
One exploration gives you a useful snapshot. Multiple explorations improve graph coverage over time.
{% endhint %}

### Continue with

* [Goals & Intent Tests](/core-concepts/goals-and-intent-tests)
* [GitHub Action](/integrations/github-actions/ci-integration-with-github-actions)
* [API Keys](/integrations/api-keys)


# Products & Targets

Understand how products and targets relate.

Products define what you test. Targets define which version you test.

### Products

A **product** is the top-level object for an application or website in Duku. It stores the name, description, and general base URL for that app or site.

Common examples:

* A marketing site
* A web app
* A checkout flow

### Targets

A **target** is a concrete deployment of a product. In most teams, targets map to commits, branches, preview deployments, or production.

Explorations run against the product's base URL. A target inherits that URL - it records which version you tested, not where.

In the app, you manage targets from the target menu in the top-right navbar, next to the active product.

Each target carries:

* **Version:** A commit SHA or build identifier
* **Metadata:** Context like branch name, PR number, and commit message
* **Environment:** For example staging, canary, or production

### Relationship

```
Product: Web App (https://www.example.com)
├── Target: main @ abc123
├── Target: PR #42 @ def456
└── Target: PR #43 @ ghi789
```

CI can override the URL for a single run, which is how per-PR preview deployments are explored.

{% hint style="info" %}
Create one product per application. Use targets for environments and deploy variants.
{% endhint %}

When you use the GitHub Action, Duku creates and updates targets automatically. When you use the API directly, call `upsertSimulationTarget`.


# Explorations & State Graphs

Learn how Duku discovers states, transitions, and graphs.

Exploration is how Duku learns your application. The state graph is the output.

### Explorations

An **exploration** is an automated browser session against a target URL. Duku launches a real browser, follows reachable interactions, and records the resulting states and transitions.

You can run multiple explorations in parallel to increase coverage.

### States

A **state** is a unique page or screen discovered during exploration. Each state includes the URL, a screenshot, and any captured errors.

### Transitions

A **transition** is an action that moves from one state to another, such as a click, form submission, or navigation event.

### State graphs

The **state graph** is a directed graph of discovered states and transitions.

Duku exposes this graph at several levels:

| Level            | What it shows                                              |
| ---------------- | ---------------------------------------------------------- |
| **Run graph**    | States and transitions from one exploration                |
| **Target graph** | The aggregate graph for a target - each run merges into it |

{% hint style="info" %}
The target graph gets more complete as you run more explorations against the same target.
{% endhint %}


# Goals & Intent Tests

Understand goals, intents, and how intent tests work.

Goals tell Duku what matters. Intent tests prove those journeys still work.

### Goals

A **goal** is a business-critical destination in the state graph, such as a checkout confirmation page, a successful login redirect, or a registration completion screen.

Duku identifies likely goals from graph structure, page characteristics, and navigation patterns.

### Intents

An **intent** is a human-readable description of a path to a goal. Duku generates descriptions like:

* *User opens the login page, enters credentials, and reaches the dashboard*
* *User adds an item to cart, completes checkout, and reaches order confirmation*

### Intent tests

**Intent tests** replay discovered journeys in a real browser. Duku selects the shortest known paths to each goal, then executes them against the target.

This gives you a simple answer to a hard question: can users still complete the journeys that matter?

{% hint style="info" %}
Intent tests are derived from exploration data, so they adapt as your application changes.
{% endhint %}

#### Run intent tests

* **From the app:** Open a product, choose a target from the top-right target menu, then click **Run Tests**
* **From the API:** Call `runAllTestCases` (or `runTestCase` for a single test)

#### Results

Each run includes:

* **Status:** Whether the journey completed
* **Step log:** Which steps passed or failed
* **Screenshots:** Visual evidence for each step
* **Errors:** Any issue captured during replay


# Issues & signals

How Duku turns the errors it finds into a short list of distinct problems.

Duku turns run errors into a concise list of distinct problems.

#### From errors to issues

A run produces errors. Errors with the same signature are collected into a **signal**. Signals with one underlying cause are collected into an **issue**.

This keeps a broken endpoint in one issue, not a dozen near-identical rows.

#### Issues and signals views

Where grouping is enabled, the dashboard offers two views:

* **Issues** shows one entry per problem.
* **Signals** shows each individual error on its own.

Use the [Issues](/viewport-dashboard/issues) to review either view.

#### Counts

An issue's **issue count** is the number of signals behind it. Its **occurrence count** is the number of distinct runs that hit it.

Occurrence count does not count every error seen during those runs.

#### Status and priority

Status and priority are set on the issue. They apply to everything grouped into it.

A status you set never changes back automatically.

Use the [Issues API](/integrations/issues-api) to read issues and signals, or write status and priority changes.


# Overview

See high-level metrics for your organisation.

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2F8qQrUhcxkst9OYad80X9%2Fduku-overview-2026-09-11.png?alt=media&amp;token=9cbe6248-2f00-4b33-be0a-8cd498bac43e" alt=""><figcaption></figcaption></figure>

### Summary

* <i class="fa-square-check">:square-check:</i> **Pass rate** - Percentage of runs passing over a window you choose (7, 14, 30, or 90 days). The trend arrow compares against the previous period so you can see whether quality is moving up or down. Environment comparison lives on the product screen.
* <i class="fa-clock-rotate-left">:clock-rotate-left:</i> **Recent runs** - The latest automated test executions with their pass rate and duration, so you can confirm the most recent activity at a glance.
* <i class="fa-triangle-exclamation">:triangle-exclamation:</i> **Open issues** - Everything currently unresolved, the most recently surfaced problems first, each with how many times it has occurred.
* <i class="fa-diagram-project">:diagram-project:</i> **Error origin** - Where failures come from: your own app, a third-party dependency, or an unknown source. This tells you how much is yours to fix.
* <i class="fa-circle-nodes">:circle-nodes:</i> **Issues by source** - How each issue was detected: console, JavaScript, or network. This tells you what kind of problem you are dealing with.
* <i class="fa-layer-group">:layer-group:</i> **Issues by priority** - Open issues bucketed into critical, high, medium, and low.
* <i class="fa-file-lines">:file-lines:</i> **Top affected pages** - The pages carrying the most open issues, ranked, so you know where to focus first.

Spot regressions quickly here, then drill into issues, products, exploration, tests, or schedules.

Jump to:

* [Issues](/viewport-dashboard/issues)
* [Products](/viewport-dashboard/products)
* [Exploration](/viewport-dashboard/exploration)
* [Tests](/viewport-dashboard/tests)
* [Schedules](/viewport-dashboard/schedules)
* [Settings](/viewport-dashboard/settings)

Manage targets from the target menu in the top-right navbar, next to the active product.

The **Runs** page combines the state graph with the runs table for the selected target.


# Issues

Review distinct problems found across your app.

The Issues page shows the problems Duku has found on your app.

Learn how Duku groups errors into issues in [Issues & signals](/core-concepts/issues-and-signals).

Read and triage issues programmatically with the [Issues API](/integrations/issues-api).

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2F8zhp3rsKdDIgAhrNNnkO%2Fduku-issues-2026-09-11.png?alt=media&amp;token=fbdce5e0-39d3-44a4-b61a-efce1ab2daa3" alt=""><figcaption></figcaption></figure>

### <i class="fa-triangle-exclamation">:triangle-exclamation:</i> What you can do

* Filter by **Open**, **Triaged**, **Resolved**, or **Ignored**
* Search by issue name
* Narrow to issues blocking your tests
* Sort by priority, recency, or occurrence count
* Update status or priority for multiple issues at once

### <i class="fa-table-list">:table-list:</i> Issue list

The issue list helps you review what matters first.

Each row shows the issue name, status, priority, occurrence count, and when it was first and last seen.

Use the list to focus on fresh regressions, long-running problems, or issues affecting test coverage.

### <i class="fa-magnifying-glass">:magnifying-glass:</i> Issue detail

Open an issue to inspect it in detail.

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2FP7lTu8cTX1fkoBoFwD0h%2Fduku-issue-detail-2026-09-11.png?alt=media&amp;token=38c861ca-49fc-473a-9c55-e60b86f35332" alt=""><figcaption></figcaption></figure>

You can review:

* The issue title and priority
* The individual signals collected into this issue
* A 14-day chart showing how often it has occurred
* A sample failure with where it happened and the relevant detail
* A side panel with status, priority, category, source, first seen, last seen, occurrence count, and affected builds

### <i class="fa-timeline">:timeline:</i> Timeline

Below the summary, the timeline shows every occurrence for that issue.

Open any timeline entry to jump to the exact run where the problem happened.


# Products

Manage the applications and websites your organisation tests.

A product is an application or website your organisation tests with Duku. It is the top-level container for your targets, explorations, and results.

### Create a product

1. Click **Create product**
2. Enter:
   * **Name:** For example, `Marketing Site` or `Web App`
   * **Description:** Optional context
   * **Base URL:** The general root URL for the product
3. Click **Create product**

Then open the product and use the target menu in the top-right navbar, next to the active product, to create the first target.

If the app needs login, add a credential set from the product's settings after you create it, and set the Base URL (or a sign-in path within it) so Duku can reach the login page, for example `https://app.example.com/signin`.

For a full walkthrough, see Create a Product.

### Selecting a product

Pick a product from the product menu in the navbar (it shows each product's name and base URL). Opening a product lands on its overview, where you can compare its targets side by side and drill into explorations, tests, and issues.


# Targets

Inspect targets, goals, and run history for a deployment.

Targets are deploy-specific views of your application. Each target usually maps to a commit, branch, preview environment, or production deploy.

There is no separate Targets page.

Open a product, then use the target menu in the top-right navbar, next to the active product, to create or switch targets.

### Create a target

After you create a product, create a target for the deployment you want to test.

Each target includes:

* **Environment:** For example staging, canary, or production
* **Version:** A commit SHA, build ID, or release identifier
* **Build number:** An optional CI build number
* **Credentials:** An optional credential set to sign in with

A target inherits its URL from its product.

### Target detail

With a target selected, open **Exploration** to inspect:

* **Merged state graph:** The aggregate graph across explorations for that target
* **Discovered pages:** Every state found so far
* **Identified goals:** Business-critical destinations extracted from the graph
* **Runs table:** Exploration and intent runs for that target

Open **Schedules** to automate recurring runs for the target.

From there, you can start new explorations or intent tests. Each exploration runs against the product's base URL, unless a per-run URL override is supplied (the CI preview flow).

{% hint style="info" %}
The target graph improves as Duku accumulates more exploration data for the same deployment.
{% endhint %}


# Exploration

Review the state graph and runs table for a target.

The **Exploration** page is where you inspect the selected target.

It combines the state graph with the runs table for that target.

### What you can review

* **State graph:** The merged graph of discovered pages and transitions
* **Runs table:** Recent exploration and test runs for the selected target
* **Run detail:** Execution logs, errors, and screenshots for an individual run

### State graph

Use the state graph to inspect what Duku discovered for the target.

The graph updates as more explorations complete.

### Runs table

Scroll down to the runs table to inspect recent runs.

Open any run from the table to inspect:

* **Execution log:** A step-by-step record of actions taken
* **State graph:** Pages visited and transitions between them
* **Errors:** JavaScript errors, network failures, and broken elements
* **Screenshots:** Captured at each state transition

### Status values

| Status        | Meaning                                    |
| ------------- | ------------------------------------------ |
| **Pending**   | Queued for execution                       |
| **Running**   | Currently executing                        |
| **Completed** | Finished successfully                      |
| **Failed**    | Finished with errors or could not complete |


# Tests

Review saved journeys, replay trends, and test runs.

The Tests page shows the saved journeys Duku replays against your app.

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2FRXXKE623AoaXHz0AxAYj%2Fduku-tests.png?alt=media&#x26;token=24e37028-843d-448e-bf6e-99265b25288f" alt="Duku tests page"><figcaption></figcaption></figure>

Use it to track trend data for journeys like sign in, checkout, and signup.

### <i class="fa-list-check">:list-check:</i> Test list

Each row shows:

* The test name
* Its pass rate and recent trend
* How many times it has run
* The last run outcome, such as **Passed**, **Failed**, **Interrupted**, or **Running**

Use **Run tests** to replay everything for the selected target.

You can also run or delete an individual test from its row.

### <i class="fa-chart-line">:chart-line:</i> What the page is for

The page focuses on trends, not one-off results.

Use it to see whether a saved journey is stable, degrading, or failing repeatedly.

### <i class="fa-circle-play">:circle-play:</i> Test detail

Open a test to inspect its detail view.

The summary strip shows:

* Pass rate
* Total runs
* Failures
* Average duration
* Last run

Below that, you can review every recorded run for the test.

### <i class="fa-film">:film:</i> Run replay

Open any run to inspect the replay itself.

Each replay includes:

* A video of the journey
* A step-by-step transcript with errors called out
* A visual path map showing which step passed or failed

### <i class="fa-bolt">:bolt:</i> Start tests

You can start tests from:

* **A product:** Choose a target from the top-right target menu, then run all tests for that target
* **The Tests page:** Replay an existing saved journey
* **The API:** Call `runAllTestCases` (or `runTestCase` for a single test)


# Schedules

Automate recurring runs for a target.

Use **Schedules** to automate recurring runs for the selected target.

Open a product, choose a target, then open **Schedules**.

<figure><img src="https://1135428273-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F2rbi6bEnpVLUQHudgXDV%2Fuploads%2FL3FT3UHm6BltLkjzUrb9%2Fduku-schedules-2026-09-11.png?alt=media&amp;token=559bb8b8-5f93-4832-bcb2-7f6bb801e5a3" alt=""><figcaption></figcaption></figure>

### What you can do

* <i class="fa-arrows-rotate">:arrows-rotate:</i> Create recurring exploration sweeps.
* <i class="fa-clock">:clock:</i> Review recurrence, status, next run, and last run.
* <i class="fa-pen-to-square">:pen-to-square:</i> Edit or delete an existing schedule.

### Schedule list

The schedule list shows:

* <i class="fa-tag">:tag:</i> **Schedule:** The schedule name
* <i class="fa-play">:play:</i> **Run type:** Shown under the schedule name - **Explore** or **Tests**.
* <i class="fa-calendar-days">:calendar-days:</i> **Recurrence:** The day, time, and timezone
* <i class="fa-toggle-on">:toggle-on:</i> **Status:** Whether the schedule is active
* <i class="fa-hourglass-half">:hourglass-half:</i> **Next run** and **Last run:** When it runs next and when it last ran

### Create a schedule

1. Click **Create schedule**.
2. Add a **Name** if you want one.
3. Choose the **Run type**.
4. Select the **Days of week**.
5. Choose the **Time of day**.
6. Choose the **Timezone**.
7. Click **Create schedule**.

### Schedule options

You can choose individual days or use quick options like **Weekdays**, **Weekends**, and **Every day**.

You can also pick a time window such as **Morning**, then choose the timezone that should control the schedule.

### What to know

* The run type is fixed when the schedule is created. To change it, create a new schedule.
* A target can have up to five active schedules.
* Time windows fire at a fixed hour in your chosen timezone: Overnight 03:00, Morning 09:00, Afternoon 15:00, Evening 21:00.

### Good uses

* Weekday morning exploration sweeps
* Weekend smoke coverage
* Recurring checks for staging or preview targets


# Settings

Manage API keys, members, and organisation settings.

Settings is where organisation admins manage access and platform credentials.

### Members

Manage your team:

* **Invite members:** Send invitations by email
* **Assign roles:** Promote users to **Admin** or keep them as **Member**
* **Remove members:** Revoke access

See [Roles & Permissions](/roles-and-permissions/overview) for the full permission matrix.

### Invitations

Pending invitations can be resent or revoked. Admin-only.

### API Keys

Generate, rotate, and revoke API keys for programmatic access. See [API Keys](/integrations/api-keys) for the full flow.

### Organisation

View and update organisation-level settings.

{% hint style="info" %}
Any member can open Settings to see the organisation and its members. Inviting people and managing API keys are admin-only.
{% endhint %}


# Choosing your integration

Pick the right Duku integration for your deployment model.

Duku can be triggered four ways: **Schedules**, the `preview` action, the `environment` action, and the Platform API. The right fit depends on how you deploy, and whether you automate it. Use the steps below to pick a route, then open the setup guide.

### Find your route

Answer these in order and stop as soon as one applies.

{% stepper %}
{% step %}

### 1. Do you automate deployments in your CI/CD pipeline?

* **No:** Use [Schedules](/viewport-dashboard/schedules). Set a recurring cadence in Viewport. No pipeline needed.
* **Yes:** Go to question 2.
  {% endstep %}

{% step %}

### 2. Do you use GitHub Actions for CI/CD?

* **No:** Use [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci). This walkthrough fits CircleCI, GitLab, Jenkins, and similar tools. Use the [GraphQL API](/integrations/graphql-api) and [API Keys](/integrations/api-keys) as references.
* **Yes:** Go to question 3.
  {% endstep %}

{% step %}

### 3. Does every pull request get its own preview environment with a unique URL?

* **Yes:** Use the [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action). This creates a per-PR target and updates one sticky PR comment automatically.
* **No:** Use the [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action). This runs against a stable, pre-created environment.
  {% endstep %}
  {% endstepper %}

If you use GitHub Actions, use [Choosing preview vs environment](/integrations/github-actions/choosing-preview-vs-environment) for the final split between `preview` and `environment`.

### Compare the routes

| Route              | Best for                                                                               | Typical trigger                                                                   | What Duku explores                     | How results surface                                                                                                                                                                                  | Set it up                                                                                                                                                                  |
| ------------------ | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Schedules          | Recurring coverage with no pipeline wiring, or no automated deploys                    | A cadence you set in Viewport                                                     | Any target you point it at             | In Viewport                                                                                                                                                                                          | [Schedules](/viewport-dashboard/schedules)                                                                                                                                 |
| Preview action     | GitHub Actions where every PR gets its own preview URL                                 | `pull_request`                                                                    | The PR's preview deployment            | Sticky PR comment that updates in place with results                                                                                                                                                 | [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action)                                                                                       |
| Environment action | GitHub Actions against a stable, pre-created environment like staging, canary, or prod | `push`, `schedule`, `workflow_dispatch`, or `pull_request` for a protected branch | A fixed target you created in Viewport | In Viewport. Duku also posts a PR comment when you run on `pull_request`, or when you pass repository and PR metadata, and the Duku GitHub App is installed. There is no merge-gating Check Run yet. | [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action)                                                                               |
| Platform API       | Any other CI, such as CircleCI, GitLab CI, or Jenkins, or a custom pipeline            | Whenever your pipeline calls it                                                   | Any URL you pass in                    | In Viewport. You can also post a PR comment when you pass PR metadata. See the tip below.                                                                                                            | [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci), [GraphQL API](/integrations/graphql-api), and [API Keys](/integrations/api-keys) |

{% hint style="info" %}
**Get PR comments without GitHub Actions**

If you use the Platform API and still want GitHub PR comments, pass `githubRepository`, `githubPrNumber`, and `serverManagedComment: true` to `startExploration`.

Duku's GitHub App posts the in-progress comment and updates it in place when the run finishes. You do not need a GitHub Action.

This only requires the Duku GitHub App to be installed on the repository. The App is a one-time install. It posts comments through the GitHub API, and uses no CI minutes.
{% endhint %}

### Under the hood

All three managed routes, **Schedules**, `preview`, and `environment`, call the same Platform API underneath. If an action does not expose a parameter you need, you can drop down to direct API calls.


# API Keys

Generate and use API keys for programmatic access.

API keys provide machine-to-machine access to the Duku platform.

Each API key is one base64-encoded string. It contains the OAuth2 client ID and secret. Exchange the key for a short-lived Bearer token, then use the token against the GraphQL API.

### Limits and scope

* Each key is scoped to one organisation.
* Each user can have up to **10 API keys**.
* Keys do not expire unless you set an expiry during creation.
* Organisation admins can create, view, and revoke keys. Rotation is available via the API.
* A key inherits the roles of the user who created it, so a key made by an admin can perform admin operations. Create keys from a user whose role matches what the integration needs.
* Listing, creating, rotating and revoking keys are all organisation-admin operations - a member cannot see the key list.
* Rotating a key returns a new secret and keeps the old one valid for five minutes, so a running deploy is not cut off mid-flight.

### Generate a key

1. Open **Settings → API Keys** in Viewport
2. Click **Generate API Key**
3. Enter a label like `GitHub Actions` or `CI Pipeline`
4. Click **Generate**
5. Copy the API key.

Over the API, `generateApiKey` returns `clientId` and `clientSecret` as separate fields, plus the `roles` the key carries. The single base64 string shown in the dashboard is `clientId:clientSecret` base64-encoded for convenience - combine them yourself if you need that form. The secret is returned once and cannot be retrieved again.

{% hint style="warning" %}
The API key is shown once. Store it in your secret manager immediately.
{% endhint %}

### Exchange the key for an access token

Base64-decode the key, then split its value on the first colon.

```bash
DECODED_KEY=$(printf '%s' '<your_api_key>' | base64 --decode)
CLIENT_ID=${DECODED_KEY%%:*}
CLIENT_SECRET=${DECODED_KEY#*:}

curl -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET"
```

Example response:

```json
{
  "access_token": "eyJhbGciOi...",
  "token_type": "Bearer"
}
```

Access tokens are short-lived, typically about five minutes. Fetch a fresh token rather than caching one for a long-running job.

### Use the access token

```bash
curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ subjects { id name } }"}'
```

### Rotate a key

Rotation is available via the API only - there is no Rotate button in Viewport yet. Call the `rotateApiKey` mutation to issue a new secret for an existing key, then store the new value (it is shown once). To retire a key from the dashboard, use **Revoke** and **Generate** a replacement.

{% hint style="info" %}
Confirm how long the previous secret keeps working after rotation before you rely on a specific grace window.
{% endhint %}

### Revoke a key

1. Open **Settings → API Keys**
2. Click **Revoke**
3. The key is deleted permanently

#### Auditing keys

The `apiKeys` query returns each key's `name`, `clientId`, `createdAt`, `expiresAt` and `lastUsedAt`, so you can find keys that have stopped being used before revoking them.

### Best practices

* Use one key per integration
* Rotate keys regularly
* Never commit secrets

### See also

Use Triggering explorations from any CI for a complete Platform API walkthrough.


# GitHub Actions

Start Duku from pull requests, deploys and scheduled workflows with the preview and environment actions.

Use the preview action for pull-request deployments and the environment action for stable targets. For merge gating, use [GitHub check runs](/integrations/github-check-runs).

* [CI integration with GitHub Actions](/integrations/github-actions/ci-integration-with-github-actions)
* [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action)
* [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action)
* [Choosing preview vs environment](/integrations/github-actions/choosing-preview-vs-environment)
* [Configuration reference](/integrations/github-actions/configuration-reference)
* [Troubleshooting](/integrations/github-actions/troubleshooting)


# CI integration with GitHub Actions

Start Duku from pull requests, deploys, and scheduled workflows.

[Not sure which integration fits? Start here.](/integrations/choosing-your-integration)

Use Duku's GitHub Actions to start explorations from your CI pipeline.

Not using GitHub Actions? See [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci) to call the Platform API directly from any pipeline.

Use `preview` for short lived pull request previews. Use `environment` for stable targets like staging or a canary deployment.

### Start here

* [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action)
* [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action)
* [Choosing preview vs environment](/integrations/github-actions/choosing-preview-vs-environment)
* [Configuration reference](/integrations/github-actions/configuration-reference)

### Which action to use

| Action        | Use it for                                                                                                                               | Trigger events                                                                                                                                                                                                                           |
| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `preview`     | Per pull request previews, where each PR gets its own short lived target. Duku resolves the PR's preview deployment URL and explores it. | `pull_request` as the primary trigger. It also runs on `push` and `workflow_dispatch`. On those triggers, it only posts a PR comment when you pass [`repository` and `pr-number`](/integrations/github-actions/configuration-reference). |
| `environment` | Stable, long lived targets you create once in Viewport, such as staging, a production canary, or a customer specific deploy.             | `push` to a long lived branch, `schedule`, and `workflow_dispatch`                                                                                                                                                                       |

### Async by design

Both actions are async by design. A Duku exploration commonly takes 5 to 40 minutes.

The workflow starts the exploration and exits within seconds. This keeps your runner free and avoids spending Actions minutes while Duku explores the app.

1. Create or reference the target.
2. Resolve the URL to explore.
3. Start the exploration. The `environment` action also starts a test-case run.
4. On a native `pull_request` event, or on another trigger when you pass `repository` and `pr-number`, post one sticky `Exploration in progress` comment.
5. Exit.

When the exploration finishes, results appear in Viewport. When comment metadata is present, the same sticky comment is updated in place with run counts and the top issues found.

{% hint style="warning" %}
Install the [Duku AI GitHub App](https://github.com/apps/duku-ai). The App posts the final PR comment update. Without it, the action still posts the running comment and exits cleanly, and the results still appear in Viewport.
{% endhint %}

### Prerequisites

1. Create a Platform API key in Viewport - **Settings → API Keys**. Store it as the repository secret `PLATFORM_API_KEY`.
2. For any run that posts a PR comment, pass `github-token: ${{ github.token }}` and set `pull-requests: write`. This includes all preview PR runs and environment runs triggered on `pull_request`.
3. Add a workflow file under `.github/workflows/`.
4. Install the [Duku AI GitHub App](https://github.com/apps/duku-ai). It is required for final PR comment updates.
5. For `environment`, create a target in Viewport first and store its ID as a repository variable like `DUKU_STAGING_TARGET_ID`.

### What to expect

GitHub Actions only starts the run. Viewport is the source of truth for final exploration and test results.

The `preview` action posts a **GitHub check run** you can require in branch protection to gate merges - this is the recommended gate, and it keeps the workflow fire-and-forget (no polling, no exit-code gating). See [GitHub check runs](/integrations/github-check-runs). The `environment` action does not post a check run yet, so gate those runs on the PR comment or Viewport results. Threshold-based gating on issue counts is not yet available.

### Read next

* [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action)
* [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action)
* [Troubleshooting](/integrations/github-actions/troubleshooting)


# Quickstart: preview action

Start Duku automatically for pull request previews.

{% hint style="warning" %}
Install the [Duku AI GitHub App](https://github.com/apps/duku-ai) on the repository. The App posts the final PR comment update. Without it, results still appear in Viewport, but the PR comment does not update with final results.
{% endhint %}

Use this workflow when each pull request gets its own preview deployment.

{% stepper %}
{% step %}

### 1. Add the required repository secret and variable

Create `PLATFORM_API_KEY` in Viewport - **Settings → API Keys**.

Add `PLATFORM_PRODUCT_ID` as a repository variable. It is not a secret.
{% endstep %}

{% step %}

### 2. Create the workflow file

Copy this file into `.github/workflows/duku-preview.yml`:

```yaml
name: Duku preview

on:
  pull_request:
    types: [opened, synchronize, reopened]
    branches: [main]

concurrency:
  group: duku-preview-${{ github.event.pull_request.number }}
  cancel-in-progress: true

jobs:
  preview:
    name: Duku preview
    runs-on: ubuntu-latest
    # The action exits in seconds; this cap only guards against network hangs.
    timeout-minutes: 5
    permissions:
      contents: read
      pull-requests: write   # required to post the sticky "running" PR comment
      deployments: read      # required for preview URL resolution
      checks: read           # for preview URL resolution
      statuses: read         # for preview URL resolution
    steps:
      - uses: actions/checkout@v4
      - name: Duku preview
        id: preview
        uses: duku-ai/actions/preview@preview/v0.2.0
        with:
          api-key: ${{ secrets.PLATFORM_API_KEY }}
          product-id: ${{ vars.PLATFORM_PRODUCT_ID }}
          # Optional: bypass Vercel Deployment Protection on protected previews.
          # vercel-automation-bypass-secret: ${{ secrets.VERCEL_AUTOMATION_BYPASS_SECRET }}
```

{% endstep %}

{% step %}

### 3. Update the workflow values

* `branches: [main]` should match the branch you merge into.
* `api-key` reads the repository secret `PLATFORM_API_KEY`.
* `product-id` reads the repository variable `PLATFORM_PRODUCT_ID`.

`pull-requests: write` lets the action post the sticky running comment.

`deployments: read`, `checks: read`, and `statuses: read` let the action resolve the preview URL.
{% endstep %}

{% step %}

### 4. Open or update a pull request

The workflow resolves the preview URL, starts the exploration, and exits within seconds.

The `concurrency` block cancels superseded runs for the same pull request.

Results appear in Viewport. The GitHub App updates the same PR comment with final results.
{% endstep %}
{% endstepper %}

PRs opened from forks cannot read repository secrets. Duku previews require a PR from a branch in the same repository.

### Preview URL resolution

On a pull request, the `preview` action figures out which deployed URL to explore.

With `preview-url-source: auto`, it checks the PR's GitHub Deployments first. It can then fall back to pull request comments from a deployment bot if you configure one.

You can force a single source. You can also set `exploration-url` to skip resolution entirely.

#### Example: resolve the URL from Vercel comments

```yaml
- uses: duku-ai/actions/preview@preview/v0.2.0
  with:
    api-key: ${{ secrets.PLATFORM_API_KEY }}
    product-id: ${{ vars.PLATFORM_PRODUCT_ID }}
    preview-url-source: 'comments'
    preview-comment-author-logins: 'vercel[bot]'
```

`auto` is usually the right choice. Use `comments` when your deployment provider does not publish a usable GitHub Deployment.

### Read next

* [Choosing preview vs environment](/integrations/github-actions/choosing-preview-vs-environment)
* [Configuration reference](/integrations/github-actions/configuration-reference)
* [Troubleshooting](/integrations/github-actions/troubleshooting)


# Quickstart: environment action

Start Duku automatically for stable environments.

Use this workflow for a target you keep in Viewport, such as staging or a production canary.

{% stepper %}
{% step %}

### 1. Create the target in Viewport

Open the product in Viewport.

Create the target from the target menu in the top-right navbar, next to the active product.

Store its ID as a repository variable such as `DUKU_STAGING_TARGET_ID`.
{% endstep %}

{% step %}

### 2. Add the repository secret

Create `PLATFORM_API_KEY` in Viewport, under **Settings → API Keys**.

Store it as the repository secret `PLATFORM_API_KEY`.
{% endstep %}

{% step %}

### 3. Create the workflow file

Copy this file into `.github/workflows/duku-environment.yml`:

```yaml
name: Duku environment

on:
  push:
    branches: [main]
  workflow_dispatch:

concurrency:
  group: duku-env-${{ vars.DUKU_STAGING_TARGET_ID }}
  cancel-in-progress: false

jobs:
  explore:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: duku-ai/actions/environment@environment/v0.3.1
        with:
          api-key: ${{ secrets.PLATFORM_API_KEY }}
          target-id: ${{ vars.DUKU_STAGING_TARGET_ID }}
```

{% endstep %}

{% step %}

### 4. Update the workflow values

* `branches: [main]` should match the branch that deploys this environment.
* `api-key` reads the repository secret `PLATFORM_API_KEY`.
* `target-id` points to the target you created in Viewport.

The action explores the product's base URL.

Set `url:` if you need to override that URL for a specific workflow run.

`cancel-in-progress: false` serializes runs per target so overlapping pushes do not create duplicate explorations. Set it to `true` if you would rather drop earlier in flight runs.
{% endstep %}
{% endstepper %}

If the target has test cases configured, the action also starts a full test-case run.

### Run on a pull request against a fixed environment

Use this mode when you merge into a protected branch, such as `prod`, and want Duku to gate the PR by exploring a permanent environment.

```yaml
name: Duku prod gate

on:
  pull_request:
    branches: [prod]

jobs:
  explore:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: duku-ai/actions/environment@environment/v0.3.1
        with:
          api-key: ${{ secrets.PLATFORM_API_KEY }}
          target-id: ${{ vars.DUKU_PROD_TARGET_ID }}
          github-token: ${{ github.token }}
```

On a native `pull_request` event, results are also posted to the PR comment when the Duku GitHub App is installed.

On `pull_request` runs, pass `github-token: ${{ github.token }}` so the action can post the sticky status comment. On `push`, `schedule`, or `workflow_dispatch`, it is not used, so the other two examples do not need it.

Without the App, the action still runs and results still appear in Viewport.

### Nightly health checks

A scheduled workflow is a good fit for nightly health checks against staging.

```yaml
name: Duku nightly staging

on:
  schedule:
    - cron: '0 3 * * *'
  workflow_dispatch:

jobs:
  explore:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: duku-ai/actions/environment@environment/v0.3.1
        with:
          api-key: ${{ secrets.PLATFORM_API_KEY }}
          target-id: ${{ vars.DUKU_STAGING_TARGET_ID }}
```

### Read next

* [Choosing preview vs environment](/integrations/github-actions/choosing-preview-vs-environment)
* [Configuration reference](/integrations/github-actions/configuration-reference)
* [Troubleshooting](/integrations/github-actions/troubleshooting)


# Choosing preview vs environment

Decide which action fits your deployment model.

[Not sure which integration fits? Start here.](/integrations/choosing-your-integration)

Choose `preview` for short lived pull request deploys. Choose `environment` for stable targets you keep around.

### Side by side

| Question                       | `preview`                                                         | `environment`                                                                                     |
| ------------------------------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| How long does the target live? | Only for the life of a pull request                               | Across many pull requests and deploys                                                             |
| How is the target managed?     | The action creates a target per PR                                | You create the target once in Viewport                                                            |
| Which URL does Duku explore?   | The resolved PR preview URL, or `exploration-url` if you pass one | The product's base URL, or `url` if you override it                                               |
| Which triggers fit best?       | `pull_request`                                                    | `push`, `schedule`, `workflow_dispatch`, and `pull_request` into a protected branch               |
| Typical examples               | Vercel previews, Netlify previews, branch deploys                 | Staging, production canary, customer demo environment, and PR gating against a stable environment |

### Rule of thumb

If you would delete the target when the pull request closes, use `preview`.

If the target survives across many pull requests, use `environment`.

If you want to gate a PR by exploring a stable environment, `environment` on `pull_request` into a protected branch is a supported mode.

### Good defaults

* Start with [Quickstart: preview action](/integrations/github-actions/quickstart-preview-action) for PR based deploy previews.
* Start with [Quickstart: environment action](/integrations/github-actions/quickstart-environment-action) for staging and nightly checks.
* Use [Configuration reference](/integrations/github-actions/configuration-reference) when you need to override URL resolution or tune test run settings.


# Configuration reference

Review inputs, outputs, secrets, and versioning.

Use this reference when you need to tune URL resolution, read action outputs, or pin a specific release.

### `preview` inputs

| Name                                   | Required | Default                            | Description                                                                                                                                                                                                                                                                                   |
| -------------------------------------- | -------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api-key`                              | Yes      | `-`                                | Platform API key. Generate in Viewport - **Settings → API Keys**.                                                                                                                                                                                                                             |
| `product-id`                           | Yes      | `-`                                | Duku product ID from Viewport - **Products**.                                                                                                                                                                                                                                                 |
| `api-url`                              | No       | `https://platform.duku.ai/graphql` | Platform API endpoint.                                                                                                                                                                                                                                                                        |
| `auth-url`                             | No       | *derived from api-url*             | Authentication server URL.                                                                                                                                                                                                                                                                    |
| `start-run`                            | No       | `true`                             | Start an exploration after creating the target. Required for the PR comment flow.                                                                                                                                                                                                             |
| `vercel-automation-bypass-secret`      | No       | *(empty)*                          | Bypass Vercel Deployment Protection on protected previews.                                                                                                                                                                                                                                    |
| `exploration-url`                      | No       | *product base URL*                 | Explore this URL instead of resolving one.                                                                                                                                                                                                                                                    |
| `preview-url-source`                   | No       | `auto`                             | How to resolve the preview URL on a PR: `auto`, `deployments`, `checks`, `statuses`, `comments`, or `none`.                                                                                                                                                                                   |
| `preview-timeout-seconds`              | No       | `60`                               | How long to wait for the preview URL to appear.                                                                                                                                                                                                                                               |
| `preview-poll-interval-seconds`        | No       | `5`                                | How often to poll for the preview URL.                                                                                                                                                                                                                                                        |
| `preview-deployment-environment-regex` | No       | *(empty)*                          | Regex matching the GitHub Deployment environment name. Defaults to a conservative `preview\|review\|staging\|pr`.                                                                                                                                                                             |
| `preview-check-name-regex`             | No       | *(empty)*                          | Regex matching a Check Run name. Skipped in `auto` if empty.                                                                                                                                                                                                                                  |
| `preview-status-context-regex`         | No       | *(empty)*                          | Regex matching a commit status context. Skipped in `auto` if empty.                                                                                                                                                                                                                           |
| `preview-comment-author-logins`        | No       | *(empty)*                          | Comma separated bot or user logins whose PR comments may carry the preview URL, for example `vercel[bot],netlify[bot]`.                                                                                                                                                                       |
| `preview-url-regex`                    | No       | *(empty)*                          | Regex to extract the preview URL from a provider comment.                                                                                                                                                                                                                                     |
| `github-installation-id`               | No       | `-`                                | Duku AI GitHub App installation ID. Normally auto discovered when the App is installed. Only set this for a custom installation map.                                                                                                                                                          |
| `repository`                           | No       | `-`                                | Repository in `owner/repo` format. Set this together with `pr-number` to attach PR metadata and post the sticky PR comment on non `pull_request` triggers.                                                                                                                                    |
| `pr-number`                            | No       | `-`                                | Pull request number. Set this together with `repository` to attach PR metadata and post the sticky PR comment on non `pull_request` triggers.                                                                                                                                                 |
| `github-token`                         | No       | *(empty)*                          | Token used to post the status PR comment (and, for preview, to resolve the preview URL). Defaults to empty; falls back to the `GITHUB_TOKEN` env var, which GitHub does not set automatically. Pass `${{ github.token }}` explicitly on `pull_request` runs. Requires `pull-requests: write`. |
| `dashboard-url`                        | No       | *(empty)*                          | Base URL of Viewport, used to build result links in the sticky comment.                                                                                                                                                                                                                       |

### `preview` outputs

| Name                   | Description                                                                                                    |
| ---------------------- | -------------------------------------------------------------------------------------------------------------- |
| `target-id`            | ID of the created target.                                                                                      |
| `target-name`          | Name of the created target.                                                                                    |
| `target-version`       | Version of the created target.                                                                                 |
| `run-id`               | ID of the started exploration. On PR events, this is the same value as `exploration-batch-id`.                 |
| `run-status`           | Kickoff status: `triggered`.                                                                                   |
| `exploration-batch-id` | ID of the exploration on PR events.                                                                            |
| `comment-id`           | Deprecated. Always empty on PR events. The status comment is now posted server-side by the Duku AI GitHub App. |

### `environment` inputs

| Name                     | Required | Default                            | Description                                                                                                                                                                                                                                                                                   |
| ------------------------ | -------- | ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `api-key`                | Yes      | `-`                                | Platform API key. Generate in Viewport - **Settings → API Keys**.                                                                                                                                                                                                                             |
| `target-id`              | Yes      | `-`                                | Pre created environment target ID from Viewport.                                                                                                                                                                                                                                              |
| `url`                    | No       | *the product's base URL*           | Override the URL this run explores. With no override, the run uses the product's base URL.                                                                                                                                                                                                    |
| `api-url`                | No       | `https://platform.duku.ai/graphql` | Platform API endpoint.                                                                                                                                                                                                                                                                        |
| `auth-url`               | No       | *derived from api-url*             | Authentication server URL.                                                                                                                                                                                                                                                                    |
| `github-installation-id` | No       | `-`                                | Duku AI GitHub App installation ID. Normally auto discovered.                                                                                                                                                                                                                                 |
| `repository`             | No       | `-`                                | Repository in `owner/repo` format. Set this together with `pr-number` to attach PR metadata and post the sticky PR comment on non `pull_request` triggers.                                                                                                                                    |
| `pr-number`              | No       | `-`                                | Pull request number. Set this together with `repository` to attach PR metadata and post the sticky PR comment on non `pull_request` triggers.                                                                                                                                                 |
| `github-token`           | No       | *(empty)*                          | Token used to post the status PR comment (and, for preview, to resolve the preview URL). Defaults to empty; falls back to the `GITHUB_TOKEN` env var, which GitHub does not set automatically. Pass `${{ github.token }}` explicitly on `pull_request` runs. Requires `pull-requests: write`. |
| `test-runs-per-path`     | No       | *platform default*                 | Override runs per path for the test-case run.                                                                                                                                                                                                                                                 |
| `test-paths-per-goal`    | No       | *platform default*                 | Override paths per goal for the test-case run.                                                                                                                                                                                                                                                |

### PR metadata on non `pull_request` triggers

Set `repository` and `pr-number` together when you run on `push`, `workflow_dispatch`, or `repository_dispatch` and still want the action to attach PR metadata and post the sticky PR comment.

This is useful when the PR or URL is only known later in the pipeline.

When you omit these inputs, behavior on a native `pull_request` event is unchanged.

### `environment` outputs

| Name              | Description                                                                                                                                               |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `run-id`          | ID of the exploration.                                                                                                                                    |
| `run-status`      | Kickoff status: `triggered` (running asynchronously - see Viewport for the final status).                                                                 |
| `test-run-id`     | ID of the test-case run. Empty if the target has no test cases configured.                                                                                |
| `test-run-status` | `triggered`, or `skipped` if the target has no test cases.                                                                                                |
| `comment-id`      | ID of the sticky PR comment. Set on a native `pull_request` event, or on other triggers when `repository`, `pr-number`, and a `github-token` are present. |

### Secrets and variables to configure

| Name                              | Kind     | Used by     | Description                                                                    |
| --------------------------------- | -------- | ----------- | ------------------------------------------------------------------------------ |
| `PLATFORM_API_KEY`                | secret   | both        | Platform API key from Viewport - **Settings → API Keys**.                      |
| `PLATFORM_PRODUCT_ID`             | variable | preview     | Duku product ID from Viewport - **Products**.                                  |
| `DUKU_STAGING_TARGET_ID`          | variable | environment | Pre created target ID from Viewport. You can use any variable name.            |
| `VERCEL_AUTOMATION_BYPASS_SECRET` | secret   | preview     | Optional bypass secret for Vercel Deployment Protection on protected previews. |

### Versioning and pinning

Each action is versioned independently with an `<action>/v<semver>` tag:

```yaml
# Pin to a specific release. Recommended for reproducibility.
- uses: duku-ai/actions/preview@preview/v0.2.0
- uses: duku-ai/actions/environment@environment/v0.3.1
```

Both actions are currently pre release in `0.x`, so pin to a specific immutable tag.

A floating major tag, such as `preview/v1`, becomes available once an action ships its first stable `v1.0.0` release.

Threshold-based merge gating (failing on issue counts) is not yet available in `0.x`. GitHub Check Run integration is available for PR-triggered explorations - see GitHub check runs. Gate merges on that required check rather than on the action exit code.

### Read next

* Quickstart: preview action
* Quickstart: environment action
* Troubleshooting


# Troubleshooting

Fix common setup and runtime issues.

Use these checks first when a workflow starts but the results are not what you expect.

### Preview run fails with "Resolving the preview URL requires a github-token" or the status PR comment never appears

Pass `github-token: ${{ github.token }}` in the action's `with:` block and grant `pull-requests: write` in the job's `permissions:` block.

The action does not receive the GitHub token automatically.

### The running PR comment appears but never updates with results

Install the [Duku AI GitHub App](https://github.com/apps/duku-ai) on the repository.

The App posts the initial running comment server-side. It also posts the final results update.

Results still appear in Viewport even if the comment never updates.

### 403 when posting the PR comment

Grant `pull-requests: write` in the job `permissions:` block.

You can also set **Settings → Actions → General → Workflow permissions** to **Read and write**.

### The preview action cannot find a preview URL

Confirm that your hosting provider creates a GitHub Deployment for the pull request.

If it does not, set `preview-url-source: comments` and set `preview-comment-author-logins` to your deployment bot, such as `vercel[bot]`.

If you already know the URL, pass `exploration-url` directly.

### The workflow does not trigger

Place the workflow file under `.github/workflows/`.

Check that the `branches:` list matches the branch you actually merge into or deploy from.

### Secrets not found or pull requests from forks fail

Pull requests from forks cannot read repository secrets.

Duku previews require a pull request from a branch in the same repository.

### The exploration runs but finds nothing or cannot reach the app

If your app sits behind a firewall, WAF, or bot manager, allowlist Duku's egress IPs.

Use [Firewall & WAF Allowlisting](/integrations/firewall-and-waf-allowlisting) to configure the allowlist.

### Read next

* [CI integration with GitHub Actions](/integrations/github-actions/ci-integration-with-github-actions)
* [Configuration reference](/integrations/github-actions/configuration-reference)


# GraphQL API

Query Duku data and trigger runs programmatically.

The Duku GraphQL API gives you programmatic access to products, targets, and runs.

### Next steps

For a complete CI walkthrough, see Triggering explorations from any CI. It covers token exchange, preview and environment flows, and pipeline gating.

### Endpoint

```
POST https://platform.duku.ai/graphql
```

### Authentication

Every request requires a Bearer token in the `Authorization` header. Use the OAuth2 client credentials flow described in API Keys.

### Request format

All requests use an HTTP `POST` with a JSON body:

```json
{
  "query": "query { subjects { id name } }",
  "variables": {}
}
```

Example request:

```bash
curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ subjects { id name } }"}'
```

{% hint style="info" %}
Example IDs such as `sms_...` (products), `smt_...` (targets), and `key_...` are placeholders.
{% endhint %}

### Queries

* [Issues API](/integrations/issues-api)

#### List products

```graphql
query {
  subjects {
    id
    name
  }
}
```

#### List targets

```graphql
query GetTargets($subjectId: ID) {
  targets(subjectId: $subjectId) {
    id
    name
  }
}
```

#### List runs for a target

Use this query to retrieve runs for a target. Arguments are `limit` (default `50`), `offset` (default `0`), `status`, `approach`, `search`, `sortBy`, and `sortDirection`. The response is `PaginatedRuns { runs, hasMore, totalCount }`.

```graphql
query GetRuns($targetId: ID) {
  runs(targetId: $targetId) {
    hasMore
    totalCount
    runs {
      id
      name
      status
      exceptionCount
      startTime
      endTime
      target {
        id
        name
      }
    }
  }
}
```

`batch(id) { runs }` returns at most the 200 most recent runs - use the top-level `runs` query when you need more.

#### Get run details

```graphql
query GetRun($id: ID!) {
  run(id: $id) {
    id
    name
    status
    exceptionCount
    startTime
    endTime
    target {
      id
      name
    }
  }
}
```

Run statuses are lowercase: `pending`, `running`, `completed`, or `failed`.

### Mutations

#### Create or update a target

Use `upsertSimulationTarget` to create a target the first time, then update it on later runs.

`buildKey` is the stable identity key for the target. Duku deduplicates by `subjectId` + `buildKey`. If you call the mutation again with the same pair, Duku updates the existing target instead of creating a duplicate.

For per-PR targets, use a stable key such as `github:repo=owner/repo:pr=123`.

Required input fields:

* `subjectId`: your Product ID
* `buildKey`: the stable identity key
* `name`: the target name shown in Viewport

Optional input fields:

* `description`
* `version`
* `environment`
* `buildNumber`
* `metadata`

A target inherits its URL from its product, so there is no target URL to set here. To explore a specific URL, pass `url` on `startExploration` (see below).

* `buildUrl` is deprecated and ignored. A target inherits its URL from its product; pass a per-run `url` on `startExploration` instead. Use `metadata` for build context - `metadata.ciRunUrl` renders as a **View CI run** link on the target.

```graphql
mutation Upsert($input: UpsertSimulationTargetInput!) {
  upsertSimulationTarget(input: $input) {
    id
    name
    version
  }
}
```

Example variables:

```json
{
  "input": {
    "subjectId": "sms_product_id",
    "buildKey": "github:repo=owner/repo:pr=123",
    "name": "PR #123"
  }
}
```

#### Start an exploration

```graphql
mutation StartExploration($input: StartExplorationInput!) {
  startExploration(input: $input) {
    id
    status
    batch {
      id
      runs {
        id
        name
      }
    }
  }
}
```

Variables:

```json
{
  "input": {
    "targetId": "smt_..."
  }
}
```

Supported input fields:

* `targetId`: target to explore
* `url`: URL to explore. For an API-key caller this per-run URL is treated as an override, and is only honoured when you also send `triggerSource: "CI"` (see below). Without it the `url` is ignored and the run uses the product's base URL (a target inherits its URL from its product).
* `githubRepository`: repository in `owner/repo` format
* `githubPrNumber`: pull request number as an integer
* `serverManagedComment`: boolean
* `triggerSource`: optional enum declaring where the run was triggered. Send `CI` from any CI provider - GitHub Actions, CircleCI, GitLab CI, and so on (the deprecated alias `GITHUB_ACTION` is treated identically). **Required whenever you pass `url`** from an API-key caller: it authorises the per-run URL override. It also labels the run as CI-triggered in your dashboards. Every other origin is derived from your credentials and cannot be declared here.
* `credentialSetId`: optional string that selects a saved credential set by ID or name. Duku resolves and decrypts it server-side, so you never send passwords in the request body. Credential fields from the set take precedence over inline values.
* `inputValues`: custom values for form filling, keyed by input type or name. Use `credentialSetId` for anything secret; values sent here travel in the request body.

When you send `serverManagedComment: true` together with `githubRepository` and `githubPrNumber`, Duku posts the PR comment itself and updates the same comment when the run finishes.

{% hint style="info" %}
**PR comments are posted by the Duku GitHub App, not GitHub Actions**

The App is a one-time install on the repository that lets Duku's platform post comments through the GitHub API. It is not GitHub Actions and uses no CI minutes.

Each target posts its own sticky comment on a PR. Duku updates that comment in place when the run finishes.
{% endhint %}

#### Run all test cases

Use `runAllTestCases` to run the test cases configured on a target.

```graphql
mutation RunAllTestCases($input: RunAllTestCasesInput!) {
  runAllTestCases(input: $input) {
    success
    intentBatchId
  }
}
```

The input requires `targetId`. It optionally accepts `credentialSetId` by ID or name. It works the same way as on `startExploration`.

#### Polling for completion

After starting a run, poll the batch until its status is `completed` or `failed`.

Batch status is lowercase - `pending` and `running` are in flight, `completed` and `failed` are terminal. Run status uses the same four values. Poll no more often than every 30 seconds; explorations typically take 5 to 40 minutes. GitHub users should use [GitHub check runs](/integrations/github-check-runs) for merge gating.

```graphql
query BatchStatus($id: ID!) {
  batch(id: $id) {
    id
    status
  }
}
```

### Trigger from any CI

Use this flow from CircleCI, GitLab CI, Jenkins, or any other pipeline. No GitHub Action is required.

1. Exchange the API key for an access token.

```bash
DECODED_KEY=$(printf '%s' "$PLATFORM_API_KEY" | base64 --decode)
CLIENT_ID=${DECODED_KEY%%:*}
CLIENT_SECRET=${DECODED_KEY#*:}

TOKEN=$(curl -s -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" | jq -r '.access_token')
```

2. Create or update the target, then capture the target ID.

```bash
TARGET_ID=$(curl -s -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation Upsert($input: UpsertSimulationTargetInput!) { upsertSimulationTarget(input: $input) { id } }",
    "variables": {
      "input": {
        "subjectId": "sms_product_id",
        "buildKey": "github:repo=owner/repo:pr=123",
        "name": "PR #123"
      }
    }
  }' | jq -r '.data.upsertSimulationTarget.id')
```

3. Start the exploration with the target ID, URL, PR metadata, and CI trigger source. `triggerSource: "CI"` is required here because a `url` is passed.

```bash
curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"query\": \"mutation StartExploration(\$input: StartExplorationInput!) { startExploration(input: \$input) { id status } }\",
    \"variables\": {
      \"input\": {
        \"targetId\": \"$TARGET_ID\",
        \"url\": \"https://preview.example.com\",
        \"githubRepository\": \"owner/repo\",
        \"githubPrNumber\": 123,
        \"serverManagedComment\": true,
        \"triggerSource\": \"CI\"
      }
    }
  }"
```

### End-to-end cURL example

```bash
DECODED_KEY=$(printf '%s' "$PLATFORM_API_KEY" | base64 --decode)
CLIENT_ID=${DECODED_KEY%%:*}
CLIENT_SECRET=${DECODED_KEY#*:}

TOKEN=$(curl -s -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d "grant_type=client_credentials" \
  -d "client_id=$CLIENT_ID" \
  -d "client_secret=$CLIENT_SECRET" | jq -r '.access_token')

curl -X POST https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "query": "mutation StartExploration($input: StartExplorationInput!) { startExploration(input: $input) { id status } }",
    "variables": {
      "input": {
        "targetId": "smt_your_target_id"
      }
    }
  }'
```

### Schema introspection

Schema introspection is disabled on the production endpoint - `__schema` and `__type` queries fail validation. Write queries against the operations documented here; if you need a schema for code generation, contact your Duku account team.

### Common errors

| Code                    | Meaning                                                                                                                                                 |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UNAUTHENTICATED`       | Missing or invalid access token                                                                                                                         |
| `FORBIDDEN`             | Insufficient permissions for this operation                                                                                                             |
| `NOT_FOUND`             | The resource does not exist, or belongs to another organisation                                                                                         |
| `BAD_USER_INPUT`        | Invalid input - including a missing URL when no override was sent and the product has no base URL                                                       |
| `CONFLICT`              | The request conflicts with the current state, such as a duplicate or a concurrent change                                                                |
| `ORG_NOT_LINKED`        | The organisation is not fully provisioned - contact support                                                                                             |
| `INTERNAL_SERVER_ERROR` | An unexpected failure. The message is always `Something went wrong. Please try again.` and `extensions.correlationId` carries an id to quote to support |

GraphQL errors are returned with HTTP 200, so check the response body for an `errors` array rather than relying on the status code. Unexpected errors are masked to `INTERNAL_SERVER_ERROR` with a fixed message; the codes above always carry a specific one.

Three common causes: sending the base64 API key as the Bearer token instead of exchanging it first returns `UNAUTHENTICATED`; naming a product that belongs to another organisation returns `NOT_FOUND`; and getting an enum's casing wrong returns no code at all - the queried field comes back `null` and the reason is in `errors[]`.


# Triggering explorations from any CI

Call the Duku Platform API directly from CircleCI, GitLab CI, Jenkins, Buildkite or any other pipeline.

Duku's GitHub Actions are thin wrappers around our Platform API. If your pipelines run somewhere else - CircleCI, Jenkins, GitLab CI, Buildkite - you can make the same API calls directly with `curl`. This page walks through the complete flow, with CircleCI as the worked example.

> **Using GitHub Actions?** Use the Duku `preview` or `environment` actions instead - they implement everything on this page for you.

What you will set up:

* Exchange your Duku API key for a short-lived access token.
* **Preview flow** - register a per-PR build and start an exploration against its preview deployment.
* **Environment flow** - start an exploration and test-case run against a long-lived environment target.
* Get results posted back to your GitHub pull requests via the Duku AI GitHub App.
* Optionally, block your pipeline until the exploration finishes.

> **Status.** This API surface is the same one our published actions are built on. Like the actions, it is pre-release - operation and field names are stable in practice but may evolve before 1.0.

### Endpoints

| What                   | URL                                                              |
| ---------------------- | ---------------------------------------------------------------- |
| Token endpoint         | `https://auth.duku.ai/realms/duku/protocol/openid-connect/token` |
| Platform API (GraphQL) | `https://platform.duku.ai/graphql`                               |

All Platform API operations are GraphQL: `POST` a JSON body of the form `{"query": "...", "variables": {...}}` with an `Authorization: Bearer <token>` header.

### Prerequisites

* A Duku API key. Generate one in **Viewport → Settings → API Keys**. The key is shown once - store it as a secret in your CI. In CircleCI, put it in a context as `DUKU_API_KEY`.
* **Preview flow:** your product ID (Viewport → Products).
* **Environment flow:** a pre-created environment target ID (created in Viewport).
* Your exploration must be set up in Viewport first. If a target has no exploration configured, `startExploration` returns an error describing what is missing - complete the setup in Viewport and retry.
* `curl` and `jq` in your job image. CircleCI's `cimg/base` image includes both.
* For PR comments: the Duku AI GitHub App installed on your repository (next section).

> **Tip.** You can list your products over the API with `query { subjects { id name } }` - products are called "subjects" in the API schema.

### Install the Duku AI GitHub App

When you pass a repository and pull-request number with an exploration, Duku posts a sticky comment on that PR - first "exploration in progress", then updated in place with the results once the exploration finishes. Those comments are posted by the Duku AI GitHub App, so you never need to handle a GitHub token in your CI job.

1. Open [github.com/apps/duku-ai](https://github.com/apps/duku-ai).
2. Click **Install** (or **Configure** if your organisation already has it) and choose the GitHub organisation that owns your repositories. If you are not an organisation owner, GitHub forwards the request to one for approval.
3. Choose **Only select repositories** and pick the repositories whose pull requests should receive Duku comments (or **All repositories**).
4. Accept the requested permissions - the app only needs to read and write pull-request comments.

That's it. There is nothing to configure on the Duku side: the platform discovers the installation automatically from the repository you name in the API call.

If the app is not installed, explorations still run normally - you just won't get PR comments. Results remain available in Viewport.

### Step 1 - Exchange your API key for an access token

See [API Keys](/integrations/api-keys) for how keys work and how to exchange one for an access token. In short: the key is a base64-encoded `clientId:clientSecret` pair; decode it, split on the first `:`, and use the standard OAuth2 client-credentials grant to get a short-lived access token.

Save the following helpers to `scripts/duku-lib.sh` in your repository - the rest of this page builds on them:

```bash
#!/usr/bin/env bash
# scripts/duku-lib.sh - helpers for calling the Duku Platform API.
# Requires: bash, curl, jq. Expects DUKU_API_KEY in the environment.

DUKU_API_URL=${DUKU_API_URL:-https://platform.duku.ai/graphql}
DUKU_AUTH_URL=${DUKU_AUTH_URL:-https://auth.duku.ai}

# Exchange the API key for a short-lived access token.
duku_token() {
  local decoded client_id client_secret
  decoded=$(printf '%s' "$DUKU_API_KEY" | base64 -d) || {
    echo 'DUKU_API_KEY is not valid base64' >&2
    return 1
  }
  client_id=${decoded%%:*}
  client_secret=${decoded#*:}
  curl -sfS -X POST "$DUKU_AUTH_URL/realms/duku/protocol/openid-connect/token" \
    -H 'Content-Type: application/x-www-form-urlencoded' \
    --data-urlencode grant_type=client_credentials \
    --data-urlencode "client_id=$client_id" \
    --data-urlencode "client_secret=$client_secret" |
    jq -r .access_token
}

# duku_gql <query> <variables-json>
# Runs a GraphQL operation. Prints the JSON response on success; prints the
# API's error messages and returns non-zero if the response contains errors.
duku_gql() {
  local query=$1 variables=${2:-'{}'} body response
  body=$(jq -n --arg q "$query" --argjson v "$variables" '{query: $q, variables: $v}')
  response=$(curl -sfS "$DUKU_API_URL" \
    -H "Authorization: Bearer $(duku_token)" \
    -H 'Content-Type: application/json' \
    -d "$body") || return 1
  if jq -e 'has("errors")' <<<"$response" >/dev/null; then
    echo 'Duku API error:' >&2
    jq -r '.errors[].message' <<<"$response" >&2
    return 1
  fi
  printf '%s\n' "$response"
}
```

> **Important.** Access tokens are short-lived (about 5 minutes). Don't fetch one token at the top of a long job and reuse it - `duku_gql` above fetches a fresh token per call, which is the simplest safe pattern.

> **Important.** GraphQL errors come back with HTTP status 200, so a bare `curl -f` will not catch them. Always check the response for an `errors` array, as `duku_gql` does.

A word on hygiene: treat the API key and access tokens as secrets. Avoid `set -x` in steps that call these helpers, and never echo the token.

{% hint style="info" %}
Avoid sending passwords in the request body. Instead of passing raw login credentials inline, create a credential set once in Viewport and pass its ID or name as `credentialSetId` on `startExploration` or `runAllTestCases`. Duku decrypts it server-side, so the secret never travels through your CI logs or the GraphQL request.
{% endhint %}

### Adapting the scripts to your CI

The helper scripts on this page use CircleCI environment variable names (`CIRCLE_*`) as examples. If you run a different CI, substitute your provider's equivalents - the GraphQL calls and the rest of each script stay exactly the same.

| What it is                  | CircleCI (used in the scripts)                              | GitHub Actions                                                      | GitLab CI               | Jenkins                                 | Buildkite                                               | Bitbucket                                   |
| --------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------- | ----------------------- | --------------------------------------- | ------------------------------------------------------- | ------------------------------------------- |
| Repository (`owner/repo`)   | `$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME`         | `$GITHUB_REPOSITORY`                                                | `$CI_PROJECT_PATH`      | derive from `$GIT_URL`, or set manually | `$BUILDKITE_ORGANIZATION_SLUG/$BUILDKITE_PIPELINE_SLUG` | `$BITBUCKET_REPO_FULL_NAME`                 |
| Pull / merge request number | parsed from `$CIRCLE_PULL_REQUEST` (or `$CIRCLE_PR_NUMBER`) | `github.event.pull_request.number`                                  | `$CI_MERGE_REQUEST_IID` | `$CHANGE_ID`                            | `$BUILDKITE_PULL_REQUEST`                               | `$BITBUCKET_PR_ID`                          |
| Commit SHA                  | `$CIRCLE_SHA1`                                              | `$GITHUB_SHA`                                                       | `$CI_COMMIT_SHA`        | `$GIT_COMMIT`                           | `$BUILDKITE_COMMIT`                                     | `$BITBUCKET_COMMIT`                         |
| Branch name                 | `$CIRCLE_BRANCH`                                            | `$GITHUB_HEAD_REF` (PRs) / `$GITHUB_REF_NAME`                       | `$CI_COMMIT_REF_NAME`   | `$BRANCH_NAME`                          | `$BUILDKITE_BRANCH`                                     | `$BITBUCKET_BRANCH`                         |
| Build number                | `$CIRCLE_BUILD_NUM`                                         | `$GITHUB_RUN_NUMBER`                                                | `$CI_PIPELINE_IID`      | `$BUILD_NUMBER`                         | `$BUILDKITE_BUILD_NUMBER`                               | `$BITBUCKET_BUILD_NUMBER`                   |
| Build / job URL             | `$CIRCLE_BUILD_URL`                                         | `$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID` | `$CI_JOB_URL`           | `$BUILD_URL`                            | `$BUILDKITE_BUILD_URL`                                  | construct from `$BITBUCKET_GIT_HTTP_ORIGIN` |

### Flow A - preview deployments

Two API calls per pipeline, run after your preview deploy step:

1. `upsertSimulationTarget` registers the build as a target under your product. The `buildKey` deduplicates reruns - every pipeline on the same PR updates the same target instead of creating a new one.
2. `startExploration` starts the exploration against the preview URL. Passing `githubRepository`, `githubPrNumber`, and `serverManagedComment: true` makes Duku post and maintain the PR comment for you.

{% hint style="warning" %}
**`triggerSource: "CI"` is required in this flow.** The per-run `url` is treated as an override, and Duku only honours it for CI callers. If you omit `triggerSource`, your API key is treated as a generic key - the `url` you send is ignored and the exploration runs against the product's configured base URL (or fails if the product has no base URL set). The script below includes it.
{% endhint %}

Unlike the GitHub Action - which discovers the preview URL from the GitHub Deployments, Checks, and Statuses APIs - your CI job supplies the preview URL directly. Your deploy step already knows it: `vercel deploy` prints it, `netlify deploy --json` returns it as `deploy_url`, or you may construct it from the PR number.

```bash
#!/usr/bin/env bash
# scripts/duku-preview.sh - register a PR build with Duku and start an
# exploration against its preview deployment.
#
# Required environment:
#   DUKU_API_KEY     Duku API key (Viewport -> Settings -> API Keys)
#   DUKU_PRODUCT_ID  Duku product ID (Viewport -> Products)
#   PREVIEW_URL      URL of the deployed preview for this PR
set -euo pipefail
source "$(dirname "$0")/duku-lib.sh"

: "${DUKU_API_KEY:?}" "${DUKU_PRODUCT_ID:?}" "${PREVIEW_URL:?}"

REPO="${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}"
PR_NUMBER=${CIRCLE_PR_NUMBER:-}
if [ -z "$PR_NUMBER" ] && [ -n "${CIRCLE_PULL_REQUEST:-}" ]; then
  PR_NUMBER=${CIRCLE_PULL_REQUEST##*/}
fi
: "${PR_NUMBER:?No pull request associated with this pipeline}"
SHORT_SHA=${CIRCLE_SHA1:0:7}

# 1. Register (or refresh) the build as a target under your product.
UPSERT='mutation UpsertSimulationTarget($input: UpsertSimulationTargetInput!) {
  upsertSimulationTarget(input: $input) { id name version }
}'
VARS=$(jq -n \
  --arg subjectId "$DUKU_PRODUCT_ID" \
  --arg buildKey "github:repo=${REPO}:pr=${PR_NUMBER}" \
  --arg name "PR #${PR_NUMBER} (${SHORT_SHA})" \
  --arg description "Build from ${CIRCLE_BRANCH:-unknown} (${SHORT_SHA})" \
  --arg version "$SHORT_SHA" \
  --argjson buildNumber "${CIRCLE_BUILD_NUM:-null}" \
  --arg ciRunUrl "${CIRCLE_BUILD_URL:-}" \
  '{input: {subjectId: $subjectId, buildKey: $buildKey, name: $name,
           description: $description, version: $version, environment: "build",
           buildNumber: $buildNumber, metadata: {ciRunUrl: $ciRunUrl}}}')
TARGET_ID=$(duku_gql "$UPSERT" "$VARS" | jq -r '.data.upsertSimulationTarget.id')
echo "Registered build as target ${TARGET_ID}"

# 2. Start the exploration. serverManagedComment makes Duku post and update
#    the sticky PR comment - no GitHub token needed in this job.
#    triggerSource: "CI" authorises the per-run url override (see note above).
START='mutation StartExploration($input: StartExplorationInput!) {
  startExploration(input: $input) { id status }
}'
VARS=$(jq -n \
  --arg targetId "$TARGET_ID" \
  --arg url "$PREVIEW_URL" \
  --arg repo "$REPO" \
  --argjson pr "$PR_NUMBER" \
  '{input: {targetId: $targetId, url: $url, githubRepository: $repo,
           githubPrNumber: $pr, serverManagedComment: true, triggerSource: "CI"}}')
RUN_ID=$(duku_gql "$START" "$VARS" | jq -r '.data.startExploration.id')
echo "Exploration started: run ${RUN_ID}"
echo "$RUN_ID" > .duku-run-id   # for the optional gate step
```

The exploration runs asynchronously - the script exits in seconds and results appear in Viewport and on the PR comment when the run finishes (typically 5 to 40 minutes).

If one pull request triggers explorations for several products, each product gets its own PR comment.

> **Vercel Deployment Protection.** If your previews are protected, pass the bypass secret when registering the build. Add `--arg bypass "$VERCEL_AUTOMATION_BYPASS_SECRET"` to the first `jq` call and `metadata: {vercelAutomationBypassSecret: $bypass}` to its input object.

> **Not on a PR?** For pushes without a pull request, use `buildKey: "github:repo=${REPO}:sha=${CIRCLE_SHA1}"` and omit `githubRepository`, `githubPrNumber`, and `serverManagedComment` - there is no PR to comment on. Keep `triggerSource: "CI"` so the preview `url` is still honoured.

Keep the `github:repo=...` `buildKey` convention shown above even though you're not on GitHub Actions: it matches what the Duku GitHub Actions generate, so if you ever run both, they deduplicate onto the same targets.

For the full list of input fields on `upsertSimulationTarget` and `startExploration`, see the [GraphQL API reference](/integrations/graphql-api).

### Flow B - environment deployments

For a staging environment, prod canary, or any pre-created environment target: skip target registration and start the run directly, then trigger the target's test cases.

```bash
#!/usr/bin/env bash
# scripts/duku-environment.sh - start an exploration and a test-case run
# against a pre-created environment target.
#
# Required environment:
#   DUKU_API_KEY    Duku API key (Viewport -> Settings -> API Keys)
#   DUKU_TARGET_ID  Environment target ID (created in Viewport)
set -euo pipefail
source "$(dirname "$0")/duku-lib.sh"

: "${DUKU_API_KEY:?}" "${DUKU_TARGET_ID:?}"

# 1. Start the exploration. With no url, the run uses the product's base URL.
START='mutation StartExploration($input: StartExplorationInput!) {
  startExploration(input: $input) { id status }
}'
VARS=$(jq -n --arg targetId "$DUKU_TARGET_ID" '{input: {targetId: $targetId, triggerSource: "CI"}}')
RUN_ID=$(duku_gql "$START" "$VARS" | jq -r '.data.startExploration.id')
echo "Exploration started: run ${RUN_ID}"
echo "$RUN_ID" > .duku-run-id

# 2. Run every test case configured on the target.
TESTS='mutation RunAllTestCases($input: RunAllTestCasesInput!) {
  runAllTestCases(input: $input) { success intentBatchId }
}'
VARS=$(jq -n --arg targetId "$DUKU_TARGET_ID" '{input: {targetId: $targetId}}')
TEST_RUN_ID=$(duku_gql "$TESTS" "$VARS" | jq -r '.data.runAllTestCases.intentBatchId')
if [ -n "$TEST_RUN_ID" ]; then
  echo "Test-case run started: ${TEST_RUN_ID}"
else
  echo "Target has no test cases configured - test run skipped"
fi
```

{% hint style="info" %}
`triggerSource: "CI"` is optional here - Flow B runs against the product's base URL, so there is no override to authorise. It is still worth sending so the run is labelled as CI-triggered in your dashboards.
{% endhint %}

* **PR-gate mode:** to run this against a fixed environment as a pull-request check and get the PR comment, add `githubRepository`, `githubPrNumber`, and `serverManagedComment: true` to the `startExploration` input, exactly as in the preview flow.
* `runAllTestCases` plans the test runs synchronously and can take up to \~90 seconds to respond; the runs themselves then execute asynchronously.

### Gate your pipeline on the result

Both flows are async by design: they kick off the run and exit, and results arrive in Viewport and the PR comment.

{% hint style="success" %}
**Recommended: gate on the GitHub check run, not by polling.** When you start the exploration with `serverManagedComment: true` and PR context - as Flow A does, and Flow B in PR-gate mode - Duku posts a **GitHub check run** you can require in branch protection. The merge is blocked or cleared automatically and your CI job can exit immediately, with no executor held open. See [GitHub check runs](/integrations/github-check-runs). Reach for polling only when you can't use branch protection - a non-GitHub host, or a gate inside the job itself.
{% endhint %}

To make the CI job itself pass or fail with the exploration - for a non-GitHub CI, or a step-level gate - poll the batch status instead. Add to `scripts/duku-lib.sh`:

```bash
# duku_wait <run-id> [timeout-seconds]
# Polls until the run reaches a terminal status. Returns 0 on completed,
# 1 on failed or timeout.
duku_wait() {
  local run_id=$1 timeout=${2:-2700} deadline status
  local query='query GetBatch($id: ID!) { batch(id: $id) { id status } }'
  deadline=$(( $(date +%s) + timeout ))
  while :; do
    status=$(duku_gql "$query" "$(jq -n --arg id "$run_id" '{id: $id}')" |
      jq -r '.data.batch.status')
    echo "$(date -u '+%H:%M:%S') run ${run_id}: ${status}"
    case $status in
      completed) return 0 ;;
      failed)    echo 'Exploration failed - see the PR comment or Viewport' >&2; return 1 ;;
    esac
    if [ "$(date +%s)" -ge "$deadline" ]; then
      echo "Timed out waiting for run ${run_id}" >&2
      return 1
    fi
    sleep 30
  done
}
```

Usage after either flow script:

```bash
source scripts/duku-lib.sh
duku_wait "$(cat .duku-run-id)"
```

* `pending` and `running` are in-flight; `completed` and `failed` are terminal.
* Explorations typically take 5 to 40 minutes - set the step timeout accordingly (the loop prints a line every 30 seconds, so CircleCI's no-output timeout won't trip).
* Polling keeps a CircleCI executor occupied for the whole run. If you only need the results, skip the gate and let the PR comment (or the check run above) deliver them.

### Putting it together in CircleCI

Store `DUKU_API_KEY` (and `DUKU_PRODUCT_ID` or `DUKU_TARGET_ID`) in a context named `duku` under **Organization Settings → Contexts**, then:

```yaml
version: 2.1

jobs:
  duku-preview:
    docker:
      - image: cimg/base:current   # includes curl and jq
    steps:
      - checkout
      # ... your existing preview deploy; capture the URL it produces ...
      - run:
          name: Deploy preview
          command: ./scripts/deploy-preview.sh > preview-url.txt
      - run:
          name: Start Duku exploration
          command: |
            export PREVIEW_URL=$(cat preview-url.txt)
            ./scripts/duku-preview.sh
      # Optional gate - remove this step for fire-and-forget behaviour
      - run:
          name: Wait for Duku results
          no_output_timeout: 20m
          command: |
            source scripts/duku-lib.sh
            duku_wait "$(cat .duku-run-id)"

workflows:
  preview:
    jobs:
      - duku-preview:
          context: duku
```

> **Note.** CircleCI only populates `CIRCLE_PULL_REQUEST` / `CIRCLE_PR_NUMBER` when the pipeline is associated with a pull request. If those are empty on your PR pipelines, check **Project Settings → Advanced → Only build pull requests**, or pass the PR number into the script yourself.

### Troubleshooting

**Token request fails with HTTP 401 (`invalid_client`).** The API key is malformed, revoked, or expired. Check that the secret contains the full base64 string from Viewport with no line breaks, or generate a new key in Viewport → Settings → API Keys.

**`Authentication required` in a GraphQL error.** The access token expired mid-job - tokens last about 5 minutes. Fetch a fresh token per call (the `duku_gql` helper does this) rather than reusing one.

**Exploration setup error.** The product or target has not been set up for exploration yet. Complete its setup in Viewport, then retry.

**`Not found` when referencing a target or product.** The ID is wrong, or it belongs to a different organisation - API keys only see resources in the organisation they were created in.

**The preview `url` is ignored / the run uses the wrong URL.** For an API-key caller, the per-run `url` is only honoured when you send `triggerSource: "CI"`. Without it the run falls back to the product's configured base URL. Add `triggerSource: "CI"` to the `startExploration` input, as shown in Flow A.

**The PR comment never appears.** Verify the Duku AI GitHub App is installed on the repository, that `githubRepository` is exactly `owner/repo`, and that `githubPrNumber` is an integer. The results comment posts only when the run finishes - check its status in Viewport if you're unsure whether it's still running.

**Duku's agents can't reach your preview.** If your site sits behind a firewall, WAF, or bot protection, see [Firewall & WAF Allowlisting](/integrations/firewall-and-waf-allowlisting). For protected Vercel previews, use the Deployment Protection bypass described above.

Need help? If anything on this page doesn't work for your setup, or you're integrating a CI provider with quirks we haven't covered, contact your Duku account team.<br>


# Firewall & WAF Allowlisting

Allowlist Duku egress IPs in your firewall, WAF, or bot-management platform.

If your site is protected by a firewall, WAF, or bot-management platform that blocks or challenges traffic by IP, you may need to allowlist Duku's egress IPs.

This lets Duku agents reach your application reliably.

### Egress IP feed

Duku publishes its current egress IPs at two stable URLs:

| URL                               | Format                              | Use it for                                    |
| --------------------------------- | ----------------------------------- | --------------------------------------------- |
| `https://ips.duku.ai/egress.json` | JSON, AWS-style envelope            | Programmatic consumption and change detection |
| `https://ips.duku.ai/egress.txt`  | Plain text, one IP per line, sorted | Firewall UIs and simple shell pipelines       |

The feed is served over HTTPS from CloudFront with a 5-minute edge cache.

### JSON format

```json
{
  "syncToken": "9f2e3a…",
  "version": "1",
  "prefixes": [
    {
      "ip_prefix": "1.2.3.4/32",
      "region": "eu-west-1",
      "environment": "prod",
      "service": "chrome-worker",
      "purpose": "egress"
    }
  ]
}
```

| Field                    | Meaning                                                                     |
| ------------------------ | --------------------------------------------------------------------------- |
| `syncToken`              | Stable hash of the current prefix set. It changes only when the IPs change. |
| `version`                | Schema version. This changes only for breaking JSON layout changes.         |
| `prefixes[].ip_prefix`   | IPv4 CIDR. Today all entries are `/32`.                                     |
| `prefixes[].region`      | AWS region the traffic egresses from, such as `eu-west-1`.                  |
| `prefixes[].environment` | `prod` or `staging`. Most customers only need `prod`.                       |
| `prefixes[].service`     | The Duku service the IP belongs to. Today this is `chrome-worker`.          |
| `prefixes[].purpose`     | `egress` - outbound traffic from Duku agents.                               |

### Quick allowlist

Fetch the plain text feed:

```bash
curl -fsSL https://ips.duku.ai/egress.txt
```

The output is one IP per line. It is sorted and ready to paste into most firewall UIs.

### Filter to a single environment

```bash
curl -fsSL https://ips.duku.ai/egress.json \
  | jq -r '.prefixes[] | select(.environment == "prod") | .ip_prefix' \
  | awk -F/ '{print $1}'
```

`egress.txt` contains every environment and is the recommended list for most customers.

### Cloudflare

#### Recommended: Skip rule on user agent

Duku agents send a `User-Agent` header that contains `Duku/1.0`.

The simplest setup is a Skip rule on User Agent. This avoids maintaining an IP list and survives IP changes automatically.

{% hint style="warning" %}
A Skip rule cannot override an IP Access Rule. If your block is defined in **Security → WAF → Tools**, you also need an IP allowlist.
{% endhint %}

#### IP allowlist with a Custom List

If your block is IP-based, create a Cloudflare Custom List of type IP.

1. Open **Manage Account → Configurations → Lists**.
2. Click **Create new list**.
3. Choose **IP**.
4. Populate the list from `https://ips.duku.ai/egress.txt`.

Reference the list in your IP Access Rule or WAF Custom Rule:

```
(ip.src in $duku_egress)
```

To keep the list current, schedule a small Cloudflare Worker or CI job that fetches the feed and updates the Rules Lists API.

```javascript
const r = await fetch("https://ips.duku.ai/egress.txt");
const ips = (await r.text()).trim().split("\n");

await fetch(
  `https://api.cloudflare.com/client/v4/accounts/${ACCOUNT_ID}/rules/lists/${LIST_ID}/items`,
  {
    method: "PUT",
    headers: {
      Authorization: `Bearer ${TOKEN}`,
      "Content-Type": "application/json"
    },
    body: JSON.stringify(ips.map((ip) => ({ ip })))
  }
);
```

Run this daily. The feed changes rarely.

### AWS WAF

Use a managed IP set:

```bash
# One-time create
aws wafv2 create-ip-set \
  --name duku-egress \
  --scope REGIONAL \
  --ip-address-version IPV4 \
  --addresses $(curl -fsSL https://ips.duku.ai/egress.txt | sed 's|$|/32|' | paste -sd, -)

# Refresh on a schedule
aws wafv2 update-ip-set \
  --name duku-egress \
  --id <ip-set-id> \
  --scope REGIONAL \
  --lock-token <current-lock-token> \
  --addresses $(curl -fsSL https://ips.duku.ai/egress.txt | sed 's|$|/32|' | paste -sd, -)
```

Then reference the IP set in a WAF rule with the **Allow** action.

### Other platforms

The feed works with any platform that can ingest an IP list over HTTPS.

Common patterns:

* `nginx` - write the IPs into an `allow` block and reload.
* `iptables` or `nftables` - fetch the list and update your rules.
* DataDome, Akamai, and Imperva - paste the list into the platform allowlist UI.

Example:

```bash
for ip in $(curl -fsSL https://ips.duku.ai/egress.txt); do
  iptables -A INPUT -s "$ip" -j ACCEPT
done
```

### Stability and change detection

The IP set is stable. It changes only when Duku's underlying networking changes.

In practice, changes are rare.

Use `syncToken` for cheap change detection. It changes if and only if the IP set changes.

Duku sends advance notice by email before planned changes.

The feed is highly available, served from CloudFront, and the URLs stay stable.

### Need help?

If allowlisting does not resolve the block, contact your Duku account team.

We can usually identify whether the block is IP-based, user-agent-based, or behavioural, and recommend the right configuration.


# GitHub check runs

Gate merges on the exploration result with a native GitHub check run.

Duku can post a **GitHub check run** on the pull request it explores - a native pass/fail check you can require in branch protection to gate merges on the exploration result. It replaces polling `batch(id) { status }` from your CI: GitHub shows the check move from *in progress* to *passed*, *failed*, or *timed out*, and the PR merge box reflects it automatically.

The check is posted by the **Duku AI GitHub App** - the same App that posts PR comments - so, as with comments, no GitHub token is handled in your CI job.

{% hint style="info" %}
The check run and the sticky PR comment are two independent surfaces built from the same exploration. The comment is the human-readable summary; the check run is the machine-readable merge gate. You can use either or both.
{% endhint %}

### Prerequisites

* The [Duku AI GitHub App](https://github.com/apps/duku-ai) installed on the repository - see [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci) for setup guidance.
* The App granted the **Checks: Read & write** permission for your organisation. If your organisation installed the App before check runs were available, GitHub prompts an organisation owner to review and approve this added permission. Until it is approved, explorations run normally but no check appears - and once it is approved, Duku retroactively creates and concludes the check for in-flight explorations, so you do not need to re-run anything.

### Enabling the check

There is no separate flag. Duku creates a check run automatically whenever an exploration is started with **server-managed PR comments** and pull-request context - that is, all three of:

* `serverManagedComment: true`
* `githubRepository` (in `owner/repo` form)
* `githubPrNumber`

If you already use the `preview` GitHub Action, or the any-CI preview flow, or the any-CI environment flow in PR-gate mode, you are already requesting the check. The `environment` GitHub Action does not request a check run.

### Making it a required check

The check appears on the pull request as:

```
Duku Exploration (<your product name>)
```

To block merges until it passes, add that check to your branch protection rule (GitHub -> **Settings -> Branches -> Branch protection -> Require status checks to pass**).

{% hint style="warning" %}
The check name is keyed to the **product name**, not the repository or PR. If you rename the product in Viewport, the check is renamed too - update your required-checks list to match, or the gate silently stops applying.
{% endhint %}

### How the result maps

| Exploration                                       | Check conclusion                                                                                                   |
| ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| Completed                                         | **success** - even when issues were found; the issues are listed in the check output rather than failing the check |
| Failed                                            | **failure**                                                                                                        |
| Still running after the timeout (default 120 min) | **timed out** - deliberately blocks the merge, because an unfinished exploration must not satisfy a required check |

* The check output summarises the run counts and the **Issues** seen on the PR (new vs. pre-existing), with a link to the exploration dashboard - the same data as the PR comment.
* If a batch finishes after it timed out, the check re-converges to the real result automatically.
* To clear a timed-out or failed check, push again or re-run the workflow: a fresh exploration supersedes the previous check on the new commit.

### Replacing the polling gate

If you currently gate with a `duku_wait`-style poll of `batch(id) { status }` (see [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci)), the check run replaces it: keep the explore call, drop the wait step, and require the `Duku Exploration (...)` check in branch protection instead. The check is maintained server-side, so your CI job can exit as soon as the exploration is kicked off.


# Issues API

Pull Duku findings into your tools and write triage decisions back.

Most teams use this to hand Duku's findings to whatever they already build with - a coding agent, a script, or their own issue tracker. Every entry says what is broken and carries the exact request, message, and step that produced it, so an agent can act on it without anyone re-describing the bug first.

All operations need a Bearer token. See [API Keys](/integrations/api-keys).

### Before you start

You need two things.

**An API key** - in the dashboard, go to **Settings → API Keys → Generate API Key**. It is shown once, so copy it straight away. A key can only see the organisation it was created in. See [API Keys](/integrations/api-keys) for how keys work.

**Your product ID** - open the product in the dashboard and take the `sms_...` value out of the URL:

```
https://app.duku.ai/p/sms_AbC123XyZ456
                      ^^^^^^^^^^^^^^^^
                      your product ID
```

### Get everything for a product in one query

```graphql
query AllIssues($subjectId: ID!) {
  issueGroups(subjectId: $subjectId, status: OPEN, sortBy: PRIORITY, sortDirection: desc) {
    groupKey
    title
    description
    bucket
    issueCount
    occurrenceCount
    lastSeenAt
    signals { id }
  }
  issuesConnection(subjectId: $subjectId, status: OPEN, first: 500, sortBy: PRIORITY, sortDirection: desc) {
    totalCount
    pageInfo { hasNextPage endCursor }
    edges { node {
        id
        title
        category
        origin
        occurrenceCount
        representativeUrl
        occurrences(first: 1) {
          runId
          sampleError {
            type message statusCode requestMethod requestUrl url
            sourceFile lineNumber columnNumber actionDescription
          }
        }
    } }
  }
}
```

`subjectId` is your product ID - the `sms_...` in the dashboard URL.

To run it, see [Save it as JSON](#save-it-as-json) below - it covers exchanging your key for a token and posting the query.

**`issueGroups`** are the problems: one entry per underlying cause, each with a written explanation and the ids of the signals behind it. **`issuesConnection`** is every individual error, with the full detail of what failed. Join them on `signals[].id` to `edges[].node.id`. See [Issues & signals](/core-concepts/issues-and-signals) for how Duku groups errors.

Both are needed - a group lists which signals belong to it, but the error detail lives on the signal. Products that are not grouped yet return `issueGroups` as an empty array and the full picture in `issuesConnection`, so the same query works either way.

### Save it as JSON

Export your key, then save the query as `query.json` with your product ID as the variable:

```bash
export DUKU_API_KEY='<paste your key here>'
```

```json
{
  "query": "query AllIssues($subjectId: ID!) { ... }",
  "variables": { "subjectId": "sms_AbC123XyZ456" }
}
```

```bash
DECODED=$(printf '%s' "$DUKU_API_KEY" | base64 --decode)
TOKEN=$(curl -s -X POST https://auth.duku.ai/realms/duku/protocol/openid-connect/token \
  -d grant_type=client_credentials \
  -d "client_id=${DECODED%%:*}" \
  -d "client_secret=${DECODED#*:}" | jq -r .access_token)

curl -s https://platform.duku.ai/graphql \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d @query.json | jq '.data' > duku-issues.json
```

Re-run it on a schedule, or after each deploy, to keep a current copy alongside your own issue data.

### What comes back

On each entry in `issueGroups`:

| Field             | What it is                                                     |
| ----------------- | -------------------------------------------------------------- |
| `title`           | A one-line statement of the problem                            |
| `description`     | What a user does, what the app does wrong, and what to look at |
| `bucket`          | Severity - `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`                 |
| `issueCount`      | How many signals are behind this entry                         |
| `occurrenceCount` | How many distinct **runs** hit it - not how many errors        |
| `lastSeenAt`      | When it was last observed                                      |
| `signals`         | The ids of the individual errors behind it                     |
| `groupKey`        | The identifier, and what `issueGroup(id:)` takes               |

On each signal in `issuesConnection`:

| Field                                                     | What it is                                                                                                    |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `id`                                                      | The signal's identifier - join it to `signals[].id`, and pass it to `updateIssueStatus`                       |
| `title`                                                   | A one-line label for this specific error                                                                      |
| `category`                                                | The kind of error, for example `api_error` or `console_error`                                                 |
| `origin`                                                  | `CLIENT_APP` for your own code, `THIRD_PARTY` for an external script, `UNKNOWN` if it could not be determined |
| `occurrenceCount`                                         | How many times this error was seen                                                                            |
| `representativeUrl`                                       | The page or endpoint it is most associated with                                                               |
| `occurrences[].runId`                                     | The run it was captured in, for looking up in the dashboard                                                   |
| `sampleError.message`                                     | The verbatim error, including the stack for JavaScript errors                                                 |
| `sampleError.type`                                        | `CONSOLE_ERROR`, `HTTP_CLIENT_ERROR`, `HTTP_SERVER_ERROR`, `JS_EXCEPTION`, `NETWORK_FAILURE`                  |
| `sampleError.actionDescription`                           | The step that triggered it, for example `Click on <button>: Delete`                                           |
| `sampleError.requestMethod` / `requestUrl` / `statusCode` | The failed request                                                                                            |
| `sampleError.sourceFile` / `lineNumber` / `columnNumber`  | Where a JavaScript error was thrown                                                                           |
| `sampleError.url`                                         | The page it happened on                                                                                       |

Which fields are filled depends on the error type - an HTTP error carries the request but no source file, a console error the reverse - so expect nulls rather than treating them as missing data.

{% hint style="info" %}
`occurrenceCount` on an entry counts distinct runs, not errors. An entry with 9 signals can report 63 - that is 63 runs affected. `type` is a string, not a fixed set, so treat unfamiliar values as "something else" rather than failing on them.
{% endhint %}

### Narrowing it down

Both queries take `status` and sort with `sortBy` (`PRIORITY`, `LAST_SEEN`, `FIRST_SEEN`, `OCCURRENCES`). Beyond that they differ:

`issueGroups` also filters on `category` and `targetId`; `issuesConnection` also filters on `search`, `url` and `origin`.

{% hint style="warning" %}
`sortDirection` is lowercase - `asc` or `desc` - while `sortBy` is uppercase. Sending `DESC` fails validation, and because the field is nullable you get a `null` result with the reason in `errors[]` rather than an obvious failure.
{% endhint %}

`first` defaults to 100 and is capped at 500 - ask for more and you silently get 500, so page with `after` from `pageInfo.endCursor` rather than raising it. `issuesConnection` is forward-only: `last` and `before` are rejected.

To pull one entry on its own, pass its `groupKey`:

```graphql
query One($id: ID!) {
  issueGroup(id: $id) {
    title
    description
    signals { id title }
    occurrences(first: 100) { runId sampleError { type message } }
  }
}
```

`occurrences(first:)` returns 100 by default and up to 1000. It gives one occurrence per run, so use `issuesConnection` when you need every signal.

### Writing back status and priority

Statuses are `OPEN`, `TRIAGED`, `RESOLVED` and `IGNORED`.

| Status     | Use it for                                        |
| ---------- | ------------------------------------------------- |
| `OPEN`     | Newly found, not yet triaged                      |
| `TRIAGED`  | Acknowledged and prioritised                      |
| `RESOLVED` | Fix deployed, not expected to recur               |
| `IGNORED`  | Known, won't-fix, or noise you do not want to see |

Set one signal's status:

```graphql
mutation Resolve($id: ID!) {
  updateIssueStatus(id: $id, input: { status: RESOLVED }) { id status }
}
```

Set it across a whole entry by passing that entry's `signals[].id` values:

```graphql
mutation ResolveMany($ids: [ID!]!) {
  bulkUpdateIssueStatus(input: { ids: $ids, status: RESOLVED }) { id status }
}
```

Set an entry's priority with `setIssueGroupPriority(id:, input: { priority: HIGH })`, using `CRITICAL`, `HIGH`, `MEDIUM` or `LOW`.

{% hint style="warning" %}
A status you set is never changed back automatically. If an issue you marked `RESOLVED` happens again, it stays `RESOLVED` - only its `lastSeenAt` and occurrence count move. It will not reappear in a `status: OPEN` query, so to catch regressions, check `lastSeenAt` on your resolved and ignored issues rather than relying on them reopening.
{% endhint %}

Status and priority writes need only the **Member** role, so an ordinary API key can triage from a pipeline.


# Overview

Understand what Members and Admins can do.

Duku uses role-based access control at the organisation level.

### Roles

| Role                      | Description                                                                                                            |
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
| **Member** (`org-member`) | Can view products, targets, runs, graphs, screenshots, and test results. Can also start explorations and intent tests. |
| **Admin** (`org-admin`)   | Includes all Member capabilities, plus member management, API key management, and organisation settings.               |

New users get the **Member** role by default. Admins can change roles from **Settings → Members**.

### Permission matrix

| Capability                        |              Member             |              Admin              |
| --------------------------------- | :-----------------------------: | :-----------------------------: |
| View products, targets, and runs  | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| View state graphs and screenshots | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| Start explorations                | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| Start intent tests                | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| View errors and test results      | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| Change issue status and priority  | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| View the API key list             |                                 | <i class="fa-check">:check:</i> |
| Manage credential sets            | <i class="fa-check">:check:</i> | <i class="fa-check">:check:</i> |
| Generate API keys                 |                                 | <i class="fa-check">:check:</i> |
| Revoke API keys                   |                                 | <i class="fa-check">:check:</i> |
| Invite members                    |                                 | <i class="fa-check">:check:</i> |
| Remove members                    |                                 | <i class="fa-check">:check:</i> |
| Change member roles               |                                 | <i class="fa-check">:check:</i> |
| Update organisation settings      |                                 | <i class="fa-check">:check:</i> |

### API key permissions

API keys are scoped to the organisation that created them and cannot cross organisation boundaries. A key also inherits the roles of the user who created it, so a key created by an admin can perform admin operations. Create keys from a user whose role matches what the integration needs.


# Release Notes

### Latest highlights

{% columns %}
{% column width="50%" %}
{% hint style="danger" icon="mobile-screen-button" %}

#### Authenticator Code Support

Credential sets can hold an authenticator secret, so Duku generates the one-time code itself on every run.
{% endhint %}
{% endcolumn %}

{% column width="50%" %}
{% hint style="info" icon="keyboard" %}

#### Agentic Inputs

Explorations now fill forms with realistic, context-aware values so runs go deeper through form-driven flows.
{% endhint %}
{% endcolumn %}
{% endcolumns %}

{% columns %}
{% column width="50%" %}
{% hint style="warning" icon="layer-group" %}

#### Grouped Issues Inbox

Issues now stay grouped by unique problem, so each one appears once and updates apply across the whole group.
{% endhint %}
{% endcolumn %}

{% column width="50%" %}
{% hint style="success" icon="circle-check" %}

#### Merge Gating with GitHub Check Runs

Explorations started from a pull request post a native GitHub check run you can require in branch protection.
{% endhint %}
{% endcolumn %}
{% endcolumns %}

### Recent releases

| Date       | Release                                                                                               | Notes                                                                                                                                                                                                                                                                                                                                                                                                                             |
| ---------- | ----------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 2026-08-27 | <i class="fa-arrow-up-wide-short">:arrow-up-wide-short:</i> **Sharper Issue Priority**                | Issue priority tracks real impact more closely. Recoverable network blips rank below genuine defects, and crawl artefacts stay out of your issue list.                                                                                                                                                                                                                                                                            |
| 2026-08-19 | <i class="fa-circle-check">:circle-check:</i> **Merge Gating with GitHub Check Runs**                 | Explorations started from a pull request post a native GitHub check run. Require it in branch protection to gate merges on the exploration result, and let your CI job exit as soon as the run is kicked off. See [GitHub check runs](/integrations/github-check-runs).                                                                                                                                                           |
| 2026-08-13 | <i class="fa-code-branch">:code-branch:</i> **CI Trigger Source for Any Pipeline**                    | Runs started from CircleCI, GitLab CI, Jenkins or any other pipeline can declare `triggerSource: CI`. It labels the run as CI-triggered in your dashboards and authorises the per-run URL override. See [Triggering explorations from any CI](/integrations/triggering-explorations-from-any-ci).                                                                                                                                 |
| 2026-08-05 | <i class="fa-diagram-project">:diagram-project:</i> **Faster Exploration Sitemap**                    | Large exploration sitemaps now open straight away, with a clearer top-to-bottom layout.                                                                                                                                                                                                                                                                                                                                           |
| 2026-08-05 | <i class="fa-clone">:clone:</i> **Issues Across Every Build**                                         | The Issues inbox and the Overview health widgets show every issue for a product, whichever build is selected. Each occurrence still links back to the build it came from.                                                                                                                                                                                                                                                         |
| 2026-07-27 | <i class="fa-link">:link:</i> **Targets Follow the Product URL**                                      | A target takes its URL from its product, so every run starts from one address you control and CI passes the deploy URL per run. Targets created by a pipeline link straight back to the CI run that made them. See [Products & Targets](/core-concepts/products-and-targets).                                                                                                                                                     |
| 2026-07-24 | <i class="fa-comments">:comments:</i> **Per-Target CI Status Comments**                               | When several targets run against the same pull request, each posts its own status comment. Every target's progress is visible at once.                                                                                                                                                                                                                                                                                            |
| 2026-07-24 | <i class="fa-key">:key:</i> **Credential Sets by Name**                                               | Runs can select a saved credential set by name or ID. Pipelines can pick the right login without embedding credentials.                                                                                                                                                                                                                                                                                                           |
| 2026-07-24 | <i class="fa-sitemap">:sitemap:</i> **Cleaner Exploration Captures**                                  | Explorations handle cookie banners and modal overlays more cleanly. They also keep list and grid pages together for a tidier sitemap and clearer screenshots.                                                                                                                                                                                                                                                                     |
| 2026-07-24 | <i class="fa-bug">:bug:</i> **Complete JavaScript Error Messages**                                    | Run errors now capture the full JavaScript exception message and every exception raised during a run. Failures are faster to diagnose.                                                                                                                                                                                                                                                                                            |
| 2026-06-30 | <i class="fa-mobile-screen-button">:mobile-screen-button:</i> **Authenticator Code Support**          | Credential sets can hold an authenticator setup secret, so Duku generates the one-time code itself on every run. A fixed code can be stored instead. SMS and push factors are not supported.                                                                                                                                                                                                                                      |
| 2026-06-30 | <i class="fa-layer-group">:layer-group:</i> **Grouped Issues Inbox**                                  | Issues now stay grouped by unique problem, so each one appears once. Update status and priority once for the whole group.                                                                                                                                                                                                                                                                                                         |
| 2026-06-24 | <i class="fa-keyboard">:keyboard:</i> **Agentic Inputs**                                              | Explorations now fill forms with realistic, context-aware values. Duku reads the page, the form, and any saved product context so runs go deeper through form-driven flows.                                                                                                                                                                                                                                                       |
| 2026-06-19 | <i class="fa-shield-halved">:shield-halved:</i> **Expanded Egress IP Feed**                           | Our published egress IP feed now lists additional source addresses for firewall and WAF allowlisting.                                                                                                                                                                                                                                                                                                                             |
| 2026-06-18 | <i class="fa-cube">:cube:</i> **WebGL Application Support**                                           | Explorations now render WebGL and canvas-based applications, expanding coverage to graphics-heavy sites.                                                                                                                                                                                                                                                                                                                          |
| 2026-06-15 | <i class="fa-inbox">:inbox:</i> **Faster Issue Triage**                                               | Sort the issues inbox by any column header, and update several issues at once from a new bulk action bar.                                                                                                                                                                                                                                                                                                                         |
| 2026-06-09 | <i class="fa-expand">:expand:</i> **Fullscreen Video Playback**                                       | Run recordings now play fullscreen and start automatically, and live streams reconnect on their own if a connection drops.                                                                                                                                                                                                                                                                                                        |
| 2026-06-09 | <i class="fa-table-columns">:table-columns:</i> **Redesigned App, Now Live**                          | The rebuilt Duku dashboard is now the default at `app.duku.ai`. It adds faster navigation, a clearer product → target layout, and a consistent header across [Overview](/viewport-dashboard/overview), [Exploration](/viewport-dashboard/exploration), [Tests](/viewport-dashboard/tests), [Schedules](/viewport-dashboard/schedules), and [Issues](/viewport-dashboard/issues). Existing dashboard links redirect automatically. |
| 2026-06-08 | <i class="fa-triangle-exclamation">:triangle-exclamation:</i> **Issue Prioritisation and Management** | [Issues](/viewport-dashboard/issues) now carry priority, which you can override. Set status and priority inline from the list or detail view, or update multiple issues at once. Issues that block a test flow rank above error-only issues so the most important problems surface first.                                                                                                                                         |
| 2026-06-05 | <i class="fa-magnifying-glass">:magnifying-glass:</i> **Sitemap Search**                              | Search the site graph by URL and jump straight to a page in [Exploration](/viewport-dashboard/exploration). Matching nodes are highlighted, the canvas pans to them, and keyboard navigation moves through results.                                                                                                                                                                                                               |
| 2026-06-04 | <i class="fa-code-branch">:code-branch:</i> **CI/CD Integration**                                     | Trigger explorations and test runs directly from your pipeline with [Duku GitHub Actions](/integrations/github-actions/ci-integration-with-github-actions). Each run posts a results summary back to the pull request and links into the app for full detail.                                                                                                                                                                     |
| 2026-06-03 | <i class="fa-trash">:trash:</i> **Delete Products and Targets**                                       | Remove products and targets you no longer need directly from [Products](/viewport-dashboard/products) and [Targets](/viewport-dashboard/targets).                                                                                                                                                                                                                                                                                 |
| 2026-06-02 | <i class="fa-vial">:vial:</i> **Run and Delete Test Cases**                                           | Run or delete an individual test case from [Tests](/viewport-dashboard/tests) or from the test-case detail view.                                                                                                                                                                                                                                                                                                                  |
| 2026-06-01 | <i class="fa-tower-broadcast">:tower-broadcast:</i> **Live Run Streaming in the New App**             | Watch explorations and test runs in real time on the run detail page as they execute, now in the redesigned app.                                                                                                                                                                                                                                                                                                                  |
| 2026-05-22 | <i class="fa-right-to-bracket">:right-to-bracket:</i> **Login Failure Detection**                     | Runs now detect when a login did not actually establish a session, including in-app login screens that look like success. Failed logins are flagged clearly, and each one captures a screenshot and page snapshot for triage.                                                                                                                                                                                                     |
| 2026-05-20 | <i class="fa-diagram-project">:diagram-project:</i> **Intent Test Run Graphs**                        | Intent test runs now render as a linear, step-by-step graph for easier path tracing. Target graphs now default to a vertical layout.                                                                                                                                                                                                                                                                                              |
| 2026-05-19 | <i class="fa-chart-line">:chart-line:</i> **Customer Health Overview**                                | [Overview](/viewport-dashboard/overview) now includes health widgets that summarise open issues and trends at a glance, plus a **Critical Open Issues** tile.                                                                                                                                                                                                                                                                     |
| 2026-05-19 | <i class="fa-inbox">:inbox:</i> **Issues Inbox**                                                      | The [Issues](/viewport-dashboard/issues) inbox is fully live with filtering, sorting, search, per-row and bulk status changes, and a filter for issues that block a test. The detail view adds an occurrence histogram, representative evidence, and a full occurrence table.                                                                                                                                                     |
| 2026-05-19 | <i class="fa-share-nodes">:share-nodes:</i> **Upgraded Sitemap**                                      | The exploration sitemap is rebuilt on a new graph engine with smoother pan and zoom, a goals panel, and click-through screenshot and error-detail modals.                                                                                                                                                                                                                                                                         |
| 2026-05-13 | <i class="fa-route">:route:</i> **Intent Flow Breaks as Issues**                                      | When an intent test breaks part-way through its prescribed flow, the break now appears as a tracked issue and is de-duplicated across builds so recurring failures stay grouped in one item.                                                                                                                                                                                                                                      |
| 2026-05-08 | <i class="fa-route">:route:</i> **Path Diverged Indicator**                                           | Test runs that wander off their prescribed path are now marked with a **Path Diverged** chip and shown as failed instead of unknown.                                                                                                                                                                                                                                                                                              |
| 2026-04-30 | <i class="fa-screwdriver-wrench">:screwdriver-wrench:</i> **Sharper Failure Triage**                  | Run detail now defaults to a signal-only error view, separates customer-facing issues from platform noise with origin badges and filter chips, and softens the failure UI when the platform itself was at fault.                                                                                                                                                                                                                  |
| 2026-04-28 | <i class="fa-circle-play">:circle-play:</i> **Video Click-to-Scrub**                                  | Click an error in a run to jump the video recording straight to the moment it occurred, with the timeline aligned to the true recording start time.                                                                                                                                                                                                                                                                               |
| 2026-04-28 | <i class="fa-shield-halved">:shield-halved:</i> **Firewall and WAF Allowlisting**                     | Duku's egress IP addresses are now published at a stable feed, and outbound traffic is tagged with a `Duku/1.0` identifier so you can allowlist Duku in your firewall or WAF. See [Firewall & WAF Allowlisting](/integrations/firewall-and-waf-allowlisting).                                                                                                                                                                     |
| 2026-04-27 | <i class="fa-wand-magic-sparkles">:wand-magic-sparkles:</i> **Autonomous Exploration**                | Add product context from **Product page → Edit** so explorations understand what your site does and what to prioritise. Explorations now choose actions more reliably across modals, overlays, and complex forms.                                                                                                                                                                                                                 |
| 2026-04-24 | <i class="fa-screwdriver-wrench">:screwdriver-wrench:</i> **Run Failure Diagnostics**                 | Failed runs now classify root cause more clearly. Platform issues and target issues are separated for faster triage.                                                                                                                                                                                                                                                                                                              |
| 2026-04-14 | <i class="fa-key">:key:</i> **Credential Sets**                                                       | Add multiple credential sets from **Product page → Credential Sets → Add Credentials**. Choose the right set at run time for different users or environments.                                                                                                                                                                                                                                                                     |
| 2026-04-08 | <i class="fa-vial">:vial:</i> **Test Cases**                                                          | Use **Tests** to **Run all** or rerun a single case. Test cases auto-generate from explorations, persist for repeat execution, and can be deleted or managed in the app.                                                                                                                                                                                                                                                          |
| 2026-04-07 | <i class="fa-diagram-project">:diagram-project:</i> **Cleaner Graph and Issue Views**                 | Use the graph viewer in **Exploration** to **Expand graph** or **Collapse graph**. Similar pages now collapse together, and duplicate issues are merged into one unique item.                                                                                                                                                                                                                                                     |
| 2026-04-06 | <i class="fa-brain">:brain:</i> **State Detection Upgrades**                                          | State detection identifies page changes more accurately with structural, semantic, and network signals.                                                                                                                                                                                                                                                                                                                           |
| 2026-04-06 | <i class="fa-tag">:tag:</i> **Test Run Badges**                                                       | Status badges reflect partial failures more accurately.                                                                                                                                                                                                                                                                                                                                                                           |
| 2026-04-01 | <i class="fa-calendar-clock">:calendar-clock:</i> **Scheduled Runs**                                  | Set repeat runs for a target from the Schedules page. Choose the days of the week and a time window, view per-schedule history, and trigger any schedule immediately.                                                                                                                                                                                                                                                             |
| 2026-03-27 | <i class="fa-arrows-rotate">:arrows-rotate:</i> **Incremental Graph Updates**                         | Target graphs now update incrementally after each exploration batch.                                                                                                                                                                                                                                                                                                                                                              |
| 2026-03-26 | <i class="fa-tower-broadcast">:tower-broadcast:</i> **Live Stream Preview**                           | Watch explorations and test runs in real time in the app.                                                                                                                                                                                                                                                                                                                                                                         |
| 2026-03-26 | <i class="fa-chart-line">:chart-line:</i> **Run Metrics**                                             | Exploration and test run metrics are now persisted for review.                                                                                                                                                                                                                                                                                                                                                                    |
| 2026-03-20 | <i class="fa-circle-play">:circle-play:</i> **Test Run Video Playback**                               | Test runs now include video recordings for easier debugging.                                                                                                                                                                                                                                                                                                                                                                      |
| 2026-03-20 | <i class="fa-right-to-bracket">:right-to-bracket:</i> **Improved Login Detection**                    | Login and authentication flow detection is now more reliable.                                                                                                                                                                                                                                                                                                                                                                     |
| 2026-03-19 | <i class="fa-bug">:bug:</i> **Failure Diagnostics**                                                   | Test runs now include clearer debugging context, structured summaries, and side-by-side path comparison.                                                                                                                                                                                                                                                                                                                          |
| 2026-03-16 | <i class="fa-eye">:eye:</i> **Improved State Detection**                                              | Accessibility-tree-based state detection now filters out visual layout noise.                                                                                                                                                                                                                                                                                                                                                     |
| 2026-03-13 | <i class="fa-filter">:filter:</i> **Pagination and Filtering**                                        | The Runs page now supports server-side pagination, sorting, search, and filtering.                                                                                                                                                                                                                                                                                                                                                |
| 2026-03-12 | <i class="fa-users">:users:</i> **Organisations and Invitations**                                     | Invite team members, assign roles, and restrict API key management to org admins and above.                                                                                                                                                                                                                                                                                                                                       |
| 2026-03-12 | <i class="fa-signal">:signal:</i> **Live Run Status**                                                 | Running explorations now show animated status indicators.                                                                                                                                                                                                                                                                                                                                                                         |
| 2026-03-10 | <i class="fa-share-nodes">:share-nodes:</i> **Target Graph**                                          | A persistent unified graph now aggregates exploration results across multiple batches.                                                                                                                                                                                                                                                                                                                                            |
| 2026-03-09 | <i class="fa-bullseye">:bullseye:</i> **Richer Goal Descriptions**                                    | Auto-generated goals now include more contextual detail from the explored application.                                                                                                                                                                                                                                                                                                                                            |
| 2026-03-05 | <i class="fa-lock">:lock:</i> **Progressive Login Support**                                           | Credential injection now handles multi-step login flows.                                                                                                                                                                                                                                                                                                                                                                          |

<details>

<summary>Older releases</summary>

| Date       | Release                                                                              | Notes                                                                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 2026-02-28 | <i class="fa-list-check">:list-check:</i> **Test Runs**                              | Full test run management now includes listing, filtering, sorting, detail views, action timelines, and per-goal test triggers.                                     |
| 2026-02-23 | <i class="fa-route">:route:</i> **Guided Exploration**                               | Path-based guided runs now replay discovered routes through your application for verification.                                                                     |
| 2026-02-20 | <i class="fa-network-wired">:network-wired:</i> **Network Activity Enrichment**      | Network capture now includes full response data, richer edge details, and automatic goal generation at the end of each exploration.                                |
| 2026-02-19 | <i class="fa-building">:building:</i> **Organisation Management**                    | Added organisation management tools in the app.                                                                                                                    |
| 2026-02-16 | <i class="fa-key">:key:</i> **Saved Credentials**                                    | Save login credentials per target with encryption at rest. Goals now appear directly on graph nodes and detail panels.                                             |
| 2026-02-13 | <i class="fa-wave-square">:wave-square:</i> **Network Activity Analysis**            | Explorations now capture and analyse network activity, including API calls and form submissions, to improve state detection and goal identification.               |
| 2026-02-11 | <i class="fa-database">:database:</i> **Persistent Exploration Results**             | Exploration results are now stored persistently, merge automatically when runs complete, and improve support for SPAs and iFrames.                                 |
| 2026-02-06 | <i class="fa-sparkles">:sparkles:</i> **AI-Powered Goal Generation**                 | Duku now generates natural-language descriptions of discovered user flows automatically. Interactive graph views are also available for selected targets and runs. |
| 2026-02-04 | <i class="fa-compass-drafting">:compass-drafting:</i> **New Exploration Strategies** | New exploration strategies improve page discovery and coverage. Custom input values and more exploration parameters are also available in the app.                 |

</details>


