Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/automations/core-actions/_examples.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ examples:
Authorization: Bearer ${{ SECRETS.exports.API_TOKEN }}
poll_interval: 5
poll_max_attempts: 24
poll_condition: "lambda response: response['data'].get('status') == 'completed'"
poll_condition: "lambda response: response['data'].get('status') != 'processing'"

http-paginate-results:
title: Follow next page links
Expand Down
80 changes: 80 additions & 0 deletions docs/automations/core-actions/_manifest.yaml
Original file line number Diff line number Diff line change
@@ -1,14 +1,94 @@
pages:
- slug: request-actions/http
title: HTTP
imports: |
import OAuthTokenExpressions from "/snippets/oauth-token-expressions.mdx";
info: |
Connect to any REST API with the `core.http_request`, `core.http_poll`, and
`core.http_paginate` actions.

## Authentication

Authenticate requests with an API key or a managed OAuth access token.

### API keys

Store API keys and static tokens as
[Secrets](/automations/core-concepts/secrets), then add the secret to
`headers`.

Pass a secret as a bearer token:

```yaml
headers:
Authorization: Bearer ${{ SECRETS.<secret_name>.<SECRET_KEY> }}
```

If the API expects a dedicated key header, use the same expression:

```yaml
headers:
X-API-Key: ${{ SECRETS.<secret_name>.<SECRET_KEY> }}
```

### OAuth

Connect an [OAuth integration](/automations/integrations/oauth-integrations),
then pass its managed access token as a bearer token.

<OAuthTokenExpressions />
actions:
- id: core.http_request
examples:
- http-request-basic
- id: core.http_poll
info: |
### Poll condition examples

`poll_condition` receives a response with `data`, `status_code`, and
`headers`. The action stops polling when the lambda returns `True`.

Prefer a negative condition that stops polling when the API leaves an
active state:

```python
lambda response: response["data"].get("status") != "processing"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Guard the negative condition against other active states

When a job API reports another nonterminal state such as queued or pending before processing, this predicate is immediately True, so core.http_poll stops and returns an unfinished job instead of polling. The same premature stop occurs when a transient response has no status value. Restrict this recommendation to APIs where processing is the only nonterminal state, or use the documented explicit terminal-state predicate.

Useful? React with 👍 / 👎.

```

This handles success, failure, cancellation, and unexpected terminal
states. A positive condition such as `status == "completed"` can keep
polling if the API stops in another state.

Keep `poll_max_attempts` above `0` to prevent indefinite polling if the
condition never matches.

If the API documents every terminal state, use `in` to match them:

```python
lambda response: response["data"].get("status") in ["completed", "failed", "cancelled"]
```

Stop on an HTTP status code:

```python
lambda response: response["status_code"] == 200
```

Stop when a response header has the expected value:

```python
lambda response: response["headers"].get("x-status") == "completed"
```
examples:
- http-poll-job
- id: core.http_paginate
info: |
<Tip>
For large lists, request a bounded page and process each batch in a
[While loop](/automations/core-actions/workflow-actions/while-loops)
until the API returns no next cursor or no items. This avoids returning
the full dataset at once.
</Tip>
examples:
- http-paginate-results

Expand Down
82 changes: 81 additions & 1 deletion docs/automations/core-actions/request-actions/http.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,44 @@
title: "HTTP"
---

import OAuthTokenExpressions from "/snippets/oauth-token-expressions.mdx";

{/* Auto-generated by scripts/generate_core_action_docs.py; do not edit by hand. */}

Connect to any REST API with the `core.http_request`, `core.http_poll`, and
`core.http_paginate` actions.

## Authentication

Authenticate requests with an API key or a managed OAuth access token.

### API keys

Store API keys and static tokens as
[Secrets](/automations/core-concepts/secrets), then add the secret to
`headers`.

Pass a secret as a bearer token:

```yaml
headers:
Authorization: Bearer ${{ SECRETS.<secret_name>.<SECRET_KEY> }}
```

If the API expects a dedicated key header, use the same expression:

```yaml
headers:
X-API-Key: ${{ SECRETS.<secret_name>.<SECRET_KEY> }}
```

### OAuth

Connect an [OAuth integration](/automations/integrations/oauth-integrations),
then pass its managed access token as a bearer token.

<OAuthTokenExpressions />

## `core.http_request`

Perform a HTTP request to a given URL.
Expand Down Expand Up @@ -145,6 +181,43 @@ Default: `true`.

Perform a HTTP request to a given URL with optional polling.

### Poll condition examples

`poll_condition` receives a response with `data`, `status_code`, and
`headers`. The action stops polling when the lambda returns `True`.

Prefer a negative condition that stops polling when the API leaves an
active state:

```python
lambda response: response["data"].get("status") != "processing"
```

This handles success, failure, cancellation, and unexpected terminal
states. A positive condition such as `status == "completed"` can keep
polling if the API stops in another state.

Keep `poll_max_attempts` above `0` to prevent indefinite polling if the
condition never matches.

If the API documents every terminal state, use `in` to match them:

```python
lambda response: response["data"].get("status") in ["completed", "failed", "cancelled"]
```

Stop on an HTTP status code:

```python
lambda response: response["status_code"] == 200
```

Stop when a response header has the expected value:

```python
lambda response: response["headers"].get("x-status") == "completed"
```

### Secrets

Optional secrets:
Expand Down Expand Up @@ -286,13 +359,20 @@ Default: `true`.
Authorization: Bearer ${{ SECRETS.exports.API_TOKEN }}
poll_interval: 5
poll_max_attempts: 24
poll_condition: "lambda response: response['data'].get('status') == 'completed'"
poll_condition: "lambda response: response['data'].get('status') != 'processing'"
```

## `core.http_paginate`

Paginate through a HTTP response.

<Tip>
For large lists, request a bounded page and process each batch in a
[While loop](/automations/core-actions/workflow-actions/while-loops)
until the API returns no next cursor or no items. This avoids returning
the full dataset at once.
</Tip>

### Inputs

<ParamField path="method" type="string" required>
Expand Down
10 changes: 3 additions & 7 deletions docs/automations/integrations/oauth-integrations.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ title: "OAuth"
description: "Connect OAuth providers to Tracecat and reference managed OAuth tokens in expressions: configure scopes, refresh credentials, and call APIs from actions and agents."
---

import RegistrySecretsAndOAuth from "/snippets/registry-secrets-and-oauth.mdx";
import OAuthTokenExpressions from "/snippets/oauth-token-expressions.mdx";

## Overview

Expand Down Expand Up @@ -34,11 +34,11 @@ After you save a custom provider, connect it (complete the OAuth flow) when usin

## Use OAuth tokens in expressions

<RegistrySecretsAndOAuth />
<OAuthTokenExpressions />
Comment thread
topher-lo marked this conversation as resolved.

## OAuth and actions

Registry actions reference OAuth integrations but do not create them. Configure a built-in or custom provider in Integrations first, then declare `RegistryOAuthSecret` (Python) or `type: oauth` (YAML) so the action requires that integration at runtime. Syntax and examples are in [Use OAuth tokens in expressions](#use-oauth-tokens-in-expressions) above.
Registry actions reference OAuth integrations but do not create them. Configure a built-in or custom provider in Integrations first, then declare `RegistryOAuthSecret` (Python) or `type: oauth` (YAML) so the action requires that integration at runtime. See the OAuth declaration syntax and examples for [Python UDFs](/custom-actions/python-udf#python-udfs) and [YAML templates](/custom-actions/yaml-template#secrets).

## OAuth and MCP

Expand All @@ -51,10 +51,6 @@ Remote MCP integrations can use an existing OAuth integration. For custom remote
## FAQ

<AccordionGroup>
<Accordion title="Why is my custom OAuth provider_id prefixed with custom_?">
When you save a custom provider, Tracecat derives a provider ID from the display name or the ID you enter (slugified). If that value does not already start with `custom_`, the server prepends `custom_` so it does not collide with [built-in providers](https://github.com/TracecatHQ/tracecat/blob/main/tracecat/integrations/providers/__init__.py). If the ID is already taken, a numeric suffix is added.
</Accordion>

<Accordion title="Can my custom action create an OAuth integration?">
No. `RegistryOAuthSecret` and YAML `type: oauth` entries declare that the action *requires* an existing OAuth integration — they do not register one. Create the integration from [Configure a provider](#configure-a-provider) (or [contribute a built-in provider](#how-do-i-contribute-a-built-in-oauth-provider)). Once it exists, your action can reference its tokens via `${{ SECRETS.<provider_id>_oauth... }}`.
</Accordion>
Expand Down
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
},
{
"group": "Integrations",
"expanded": true,
"pages": [
"automations/integrations/prebuilt-credentials",
"automations/integrations/oauth-integrations",
Expand Down
50 changes: 50 additions & 0 deletions docs/snippets/oauth-token-expressions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: "OAuth token expressions"
description: "Reference OAuth provider IDs and managed tokens in Tracecat expressions."
---

OAuth expressions use the provider's exact ID, not its display name.
Comment thread
topher-lo marked this conversation as resolved.

- Built-in providers use stable lowercase IDs assigned by Tracecat, with
underscores between words, such as `slack`, `google_drive`, and
`microsoft_sentinel`.
- Custom providers use an ID derived from the provider name, or from the
requested ID when you create one through the API. Tracecat slugifies it with
underscores and prepends `custom_`. `My Security API` becomes
`custom_my_security_api`. If that ID is already used for the same grant type,
Tracecat appends `_1`, `_2`, and so on.

Append `_oauth` to the exact provider ID for the secret name. For the key,
uppercase the complete provider ID, preserve its underscores and any numeric
suffix, then append `_USER_TOKEN` for `authorization_code` or `_SERVICE_TOKEN`
for `client_credentials`.

```yaml
# authorization_code grant
${{ SECRETS.<provider_id>_oauth.<PROVIDER_ID_UPPER>_USER_TOKEN }}

# client_credentials grant
${{ SECRETS.<provider_id>_oauth.<PROVIDER_ID_UPPER>_SERVICE_TOKEN }}
```

A built-in `google_drive` authorization-code provider and a custom
`custom_my_security_api` client-credentials provider resolve as:

```yaml
${{ SECRETS.google_drive_oauth.GOOGLE_DRIVE_USER_TOKEN }}
${{ SECRETS.custom_my_security_api_oauth.CUSTOM_MY_SECURITY_API_SERVICE_TOKEN }}
```

When either grant type is allowed, use a fallback:

```yaml
${{ SECRETS.microsoft_sentinel_oauth.MICROSOFT_SENTINEL_USER_TOKEN || SECRETS.microsoft_sentinel_oauth.MICROSOFT_SENTINEL_SERVICE_TOKEN }}
```

<Info>
Tracecat refreshes expiring authorization-code tokens when the provider
issued a refresh token, and reacquires client-credentials tokens with the
stored client credentials. The expression resolves only to the current
access-token string, which may be a JWT or an opaque token, not the refresh
token.
</Info>
18 changes: 4 additions & 14 deletions docs/snippets/registry-secrets-and-oauth.mdx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import OAuthTokenExpressions from "/snippets/oauth-token-expressions.mdx";

Use the same `${{ SECRETS... }}` syntax as workflow actions.

Custom secrets (API keys, SSH, mTLS, CA bundles, and so on):
Expand All @@ -6,21 +8,9 @@ Custom secrets (API keys, SSH, mTLS, CA bundles, and so on):
${{ SECRETS.<secret_name>.<KEY> }}
```

OAuth tokens live under `<provider_id>_oauth`. The key is the provider ID in uppercase plus `_USER_TOKEN` or `_SERVICE_TOKEN`:

- `authorization_code`: `${{ SECRETS.<provider_id>_oauth.<PROVIDER_ID_UPPER>_USER_TOKEN }}`
- `client_credentials`: `${{ SECRETS.<provider_id>_oauth.<PROVIDER_ID_UPPER>_SERVICE_TOKEN }}`

```yaml
${{ SECRETS.slack_oauth.SLACK_USER_TOKEN }}
${{ SECRETS.google_docs_oauth.GOOGLE_DOCS_SERVICE_TOKEN }}
```

Optional fallback when either grant type is allowed:
## OAuth

```yaml
${{ SECRETS.microsoft_sentinel_oauth.MICROSOFT_SENTINEL_USER_TOKEN || SECRETS.microsoft_sentinel_oauth.MICROSOFT_SENTINEL_SERVICE_TOKEN }}
```
<OAuthTokenExpressions />

## Python UDFs

Expand Down
10 changes: 8 additions & 2 deletions scripts/generate_core_action_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,10 +286,16 @@ def _render_page(
f'title: "{title}"',
"---",
"",
AUTO_GENERATED_COMMENT,
"",
]

if imports_block := page.get("imports"):
lines.extend([str(imports_block).strip(), ""])

lines.extend([AUTO_GENERATED_COMMENT, ""])

if info_block := page.get("info"):
lines.extend([str(info_block).strip(), ""])

for action_entry in action_entries:
action_id = action_entry["id"]
action = actions_by_id[action_id]
Expand Down
Loading