# Installed-app actions

Discover the integrations installed in your dataset and execute an action through its configured connection. Production requests use `https://api.quake.dev/core`, with a Quake OAuth bearer token. See [delegated sessions](https://api.quake.dev/documentation/guides/delegated-openapi-sessions) to choose app-only or user-delegated authorization and configure the token's scopes.

## Discover before executing

| Request (relative to `/core`) | Result |
| --- | --- |
| `GET /apps/installations?search=<text>` | `{ data, offset, count, total }` for visible installed apps, each including action summaries. Currently returns the whole visible set with `offset: 0`; there are no limit/offset query parameters. |
| `GET /apps/installations/{installedAppId}/actions` | Array of visible action summaries (`id`, `name`, `description`), with optional plain-text `search`. |
| `GET /apps/installations/{installedAppId}/actions/{actionId}` | Action details: `input_schema`, `output_schema`, and optional `app_ai_instructions`. |

Use `installed_id` from installed-app discovery as `{installedAppId}` and the discovered action `id` as `{actionId}`. `origin_id` identifies the underlying app definition; it is not the dataset installation ID. The response's app `id` currently matches `installed_id`, including built-in/native apps, but using the explicit field avoids confusing a public catalog ID with a configured installation.

The unauthenticated app catalog (`/apps`, `/apps/{appId}/actions`) describes published definitions, not which dataset connection to execute. Do not infer installation IDs from catalog IDs or names. Use discovery in the target dataset and preserve IDs exactly.

### Dynamic permissions

`integrations:read` allows discovery of installed apps and action details; it does **not** grant execution. Without broad discovery access, app-specific scopes filter discovery to permitted actions. Execute and poll with a scope matching the installation and action:

```text
app-<installedAppId>:<actionId>
app-<installedAppId>:*
```

Prefer the specific action scope. These are dynamic scope strings, not a fixed list of catalog actions. A delegated session's granted scopes are also constrained by the user's dataset role; requesting a scope in OAuth does not ensure consent grants it. Polling requires permission for the action that created the run, not just knowledge of `run_id`. A 403 can indicate an unavailable grant; use `/session` for delegated grants and reauthorize if required.

## Construct parameters from the schema

`input_schema` is an array of schema entries by `location`. Validate each input against that entry's JSON Schema and send it inside `parameters`, preserving its location. The public request accepts `arguments`, `body`, `query`, `formData`, `path` and `header`; supply only locations actually declared by that action. Native actions commonly use `arguments`; HTTP integrations can use several locations.

For an action discovered with `{ "location": "arguments", "schema": { "type": "object", "properties": { "query": { "type": "string" } }, "required": ["query"] } }`, the execution body would be:

```json
{
  "parameters": [{ "location": "arguments", "input": { "query": "engineer" } }],
  "execution_mode": "sync"
}
```

This is an illustrative schema, not a universal action. For an action with separate `path` and `body` entries, use separate parameter objects. Do not put action fields at the execution body's top level or wrap every action in `arguments`. The runner obtains query `arrayFormat` / per-property `arrayFormats` and `formDataEncoding` from the installed action definition; these belong to discovery schema entries, not execution parameter objects.

`input_schema` or `output_schema` can be null when a definition cannot be serialized. Null does not mean "accept arbitrary inputs" or "no parameters". Consult the app's documented contract or ask its owner for the supported shape before executing. Do not invent a schema from its name. Client-side schema validation improves feedback but is not a guarantee about the upstream API's behavior.

## Synchronous execution

POST `/core/apps/installations/{installedAppId}/actions/{actionId}/execute` with the JSON parameter envelope and bearer token. Omitting `execution_mode` selects synchronous execution. A successful HTTP 200 returns:

```json
{
  "is_native": false,
  "durationMs": 250,
  "request": {},
  "response": {
    "status": 200,
    "headers": { "content-type": "application/json" },
    "body": { "items": [{ "id": "upstream-record-123" }] }
  }
}
```

This illustrates an HTTP action: its `response` is a transport envelope `{ status, headers, body }`. The action's `output_schema` describes the response **body**, so read/validate `response.body` against it. Sandbox-code actions also return this envelope, with `status: 200`, `headers: {}`, and the handler's return value in `body`.

Native actions usually put their data directly in `response`; native actions that explicitly return an HTTP result retain its HTTP response envelope. Inspect the action-specific response shape rather than choosing a data path solely from `is_native`.

`request` is nullable and is diagnostic data, not a retry payload. Treat results as potentially sensitive; avoid logging credentials, personal data or full upstream requests. An upstream HTTP action failure is mapped to HTTP 422. Missing installation/action returns 404, insufficient execution scope 403, and unexpected failures can return 500. Inspect the response message and verify downstream state for mutations rather than assuming every error proves nothing happened.

## Asynchronous execution and polling

Set `execution_mode: "async"` for background execution. `callback_url` is optional and is allowed only in async mode.

```json
{
  "parameters": [{ "location": "arguments", "input": { "query": "engineer" } }],
  "execution_mode": "async"
}
```

HTTP 202 acknowledges acceptance, not execution success:

```json
{
  "run_id": "<run ID>",
  "status": "queued",
  "status_url": "https://api.quake.dev/core/apps/installations/action-runs/<run ID>",
  "callback_url": null,
  "created_at": "2026-01-01T00:00:00.000Z"
}
```

Save `run_id` and `status_url` before leaving the submission flow. GET the returned status URL using the same dataset token and matching action grant. Ensure the URL belongs to your configured Quake API origin before forwarding a bearer token. Poll with bounded backoff and a deadline appropriate to the action; no completion-time SLA is implied.

| Run status | Meaning |
| --- | --- |
| `queued`, `running` | Non-terminal; keep the run ID and poll later. |
| `completed` | Terminal execution envelope is in `result`. For HTTP/code actions, data is in `result.response.body`; for direct native actions, in `result.response`. |
| `failed` | Terminal failure; inspect `error.message`, `statusCode`, `code`, and `name` when present. Side effects may already have occurred. |
| `canceled` | Terminal status supported by the response schema; this public surface has no run-cancel operation. |

Status also includes `created_at`, `updated_at`, `started_at`, `finished_at` and a separate `callback` state. Each async run is assigned a retention deadline seven days after creation; save the results you need rather than depending on indefinite status availability. Large results can be stored internally and loaded by the status endpoint; clients still read `result`, not an internal object-storage pointer.

Submission currently rejects with 429 when the dataset already has 10 running or 100 queued installed-action runs. These are admission thresholds, not promises of execution concurrency or general API rate limits. Reduce submission concurrency and back off; consult known run status before resubmitting.

## Optional callbacks and their guarantees

If supplied, `callback_url` must pass public HTTP(S) URL validation; private/internal destinations and the corresponding Quake execution/status endpoints are rejected. Prefer a direct HTTPS receiver you control, avoiding redirects. After terminal execution, Quake attempts to POST JSON:

```json
{
  "run_id": "<run ID>",
  "status": "completed",
  "result": { "is_native": true, "durationMs": 250, "request": null, "response": {} }
}
```

For failure, `result` is null and `error` contains the execution error. Current delivery uses `Content-Type: application/json`, up to five attempts, and a 30-second timeout per attempt. There is **no callback signature or authorization header** in the current sender and no public field for a callback secret. Treat incoming callback data as an untrusted notification: match `run_id` against a run you recorded, fetch its status from your configured Quake API using your own token, then act on that authenticated result. An unknown run must not trigger work. This advice does not add a new sender authentication mechanism.

`callback.status` is one of `not_requested`, `pending`, `delivered`, `failed`. Its attempt counts, last HTTP status/error, next attempt and delivered timestamp describe delivery separately from execution. `delivered` records a successful HTTP response from the receiver; it does not prove the receiver finished processing. Callback failure does not change a completed action into failed execution. Delivery can repeat, especially when a response is lost; deduplicate receiver processing by recorded `run_id` and verify status. Do not rerun the action to repair callback delivery.

## Timeouts and ambiguous mutation failures

The public execution body is strict and has **no idempotency field**. Every new async POST creates a new run; a new submission is not a retry of the old run. There is no public run-list/search operation here to recover an ID lost before the 202 response.

If you have a run ID, poll it rather than reposting. If a submission times out before receiving its ID, or synchronous execution disconnects, inspect the external system's records and use an action-specific business identifier/read operation when available. Resolve uncertainty before retrying a write. Neither a transport failure nor `failed` guarantees rollback or absence of partial effects. If upstream APIs support their own deduplication field, use it only when that discovered action schema explicitly exposes it; Quake does not supply a generic exactly-once guarantee.

## Implementation references

Current behavior is defined by `src/api/openapi/routes/installed-apps.route.ts`, `installed-app-actions.route.ts`, `services/installed-app-action-authorization.service.ts`, `installed-app-action-execution.service.ts`, `installed-app-action-async-runs.service.ts`, and `services/webhooks.service/webhooks.ts` (`sendCallback`). Rediscover definitions and consult the current operation schemas after configuration changes; this guide does not establish a separate versioning or compatibility policy.
