← Plugin catalog
Developer Tools

CrowdStrike Falcon Foundry

CrowdStrike v1.5.0

Publisher description

From the marketplace listing

Create, validate, deploy, and troubleshoot Falcon Foundry apps with specialized guidance for UI, functions, collections, Falcon Fusion SOAR workflows, security, and OpenAPI integrations.

Language: English · Automatically detected from descriptions.

Files & skills

File archives

Plugin package67 files · 179 KBBrowse files →
Skill instructions
api-integrations17 KB

View saved version →

---
name: api-integrations
description: Expose external APIs to Falcon Foundry via OpenAPI specs. TRIGGER when user asks to "create an API integration", "adapt an OpenAPI spec for Foundry", "expose an API to workflows", "connect to a third-party API", or runs `foundry api-integrations create`. Also trigger when user has an OpenAPI/Swagger spec and wants it working in Falcon Foundry. DO NOT TRIGGER when user wants to call Falcon platform APIs from function code — use functions-falcon-api instead.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, openapi, api, workflows]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: integration
---

# Foundry API Integrations

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry API Integrations specialist**.
>
> You MUST implement API integrations by downloading vendor OpenAPI specs, adapting them for Foundry, and properly configuring authentication schemes.
>
> **Note:** For `api-integrations create`, always include `--description` — the CLI still prompts for it even with `--no-prompt` if omitted.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

This skill covers exposing external APIs (third-party services or CrowdStrike Falcon APIs) to the Falcon Foundry platform via OpenAPI/Swagger specifications. These integrations make API operations available to Falcon Fusion SOAR workflows, Foundry UI extensions, Foundry Functions, and other Foundry capabilities.

**API integrations are how Foundry manages credentials.** There is no secrets system, no encrypted env vars, and no key vault. When you register an API integration, the platform collects credentials at install time and manages tokens automatically. This is why functions MUST call third-party REST APIs through `APIIntegrations().execute_command_proxy()` — not via raw HTTP with env vars.

For calling Falcon APIs from within Function code, see **functions-falcon-api** instead.

## Decision Tree

```
What kind of API integration?

External API (Okta, VirusTotal, ServiceNow, etc.)
├── Vendor publishes OpenAPI spec → Download it, adapt for Foundry
└── No vendor spec available      → Write minimal spec as last resort

CrowdStrike Falcon API
└── From functions → use functions-falcon-api instead
    From workflows → use CrowdStrike auto-auth (no spec needed)
```

## Workflow: Download, Adapt, Register

**Always follow this order.** Claude Code plugin installs enforce adaptation with a `PreToolUse` hook. Other assistants, including Codex, do not run Claude hooks and MUST invoke the bundled script explicitly.

### 1. Download the vendor's spec

**NEVER write an OpenAPI spec from scratch when the vendor publishes one.** Hand-written specs produce incorrect response schemas, miss required parameters, and lack proper security definitions. Download the vendor's spec even if it has hundreds of endpoints — Foundry handles large specs fine. Do NOT rationalize writing a "focused" or "minimal" spec because the vendor spec is big.

1. **Ask the user** if they have a local copy or know where to download it
2. **Browse the repo first, don't guess URLs.** Use `gh api repos/{owner}/{repo}/git/trees/master --jq '.tree[].path'` to find the spec file, then download with `curl`. Never try multiple URLs hoping one works.
3. **Search locally** for existing specs
4. If the vendor does not publish a spec, only then write a minimal one

**Do NOT delegate spec download to Explore agents or subagents.** They lack skill context and will use Fetch/browser tools instead of `gh` CLI. Download specs inline using `gh` and `curl` as shown in the reference file.

For detailed download commands and structural fix patterns, see [references/spec-adaptation-examples.md](references/spec-adaptation-examples.md).

### 2. Adapt the spec for Foundry (MANDATORY)

```bash
python3 /path/to/foundry-skills/skills/api-integrations/scripts/adapt_spec_for_foundry.py /tmp/VendorApi.yaml

# Preview changes without writing
python3 /path/to/foundry-skills/skills/api-integrations/scripts/adapt_spec_for_foundry.py /tmp/VendorApi.yaml --dry-run
```

> **Dependencies:** The script requires `pyyaml`, and installing the plugin does not install Python packages. A bare `pip install` fails on Homebrew and system Pythons with `externally-managed-environment` (PEP 668), so set up a venv once in the checkout and run the script from it:
>
> ```bash
> SKILLS_REPO=/path/to/foundry-skills
> python3 -m venv "$SKILLS_REPO/.venv"
> "$SKILLS_REPO/.venv/bin/pip" install -r "$SKILLS_REPO/requirements.txt"
> "$SKILLS_REPO/.venv/bin/python" "$SKILLS_REPO/skills/api-integrations/scripts/adapt_spec_for_foundry.py" /tmp/VendorApi.yaml
> ```

The helper is bundled with this skill at `scripts/adapt_spec_for_foundry.py`. Resolve that path relative to this `SKILL.md`, not the app workspace.

The script applies fixes derived from 12 production Foundry sample apps:
- **Swagger 2.0 conversion**: Converts to OpenAPI 3.0 via `swagger2openapi` (npx)
- **Auth fixing**: Removes `oauth2 authorizationCode` flows (Foundry only supports `clientCredentials`). Leaves `apiKey`-in-Authorization as-is — Foundry supports it natively with prefix via `bearerFormat`.
- **Server URLs**: Strips `https://` from variable-based URLs (Foundry adds protocol separately). Removes `default` from variables without `enum` (prevents locked dropdown).
- **Parameter dedup**: Removes operation-level parameters that duplicate path-level parameters (prevents Foundry's "items are equal" validation error).

Foundry's UI import handles large/complex specs. Don't trim or simplify vendor specs. The auth fixes are what matter.

### 3. Register with the CLI

```bash
foundry api-integrations create --name "VendorApi" --description "Vendor API" --spec /tmp/VendorApi.yaml --no-prompt
```

> **Always include `--description`** with `api-integrations create`. Even with `--no-prompt`, the CLI still interactively prompts for the optional description if omitted, causing `Error: EOF`.

**Done.** For most integrations, this is all you need. Validate immediately after registering (`foundry apps validate --no-prompt`).

Only add `x-cs-operation-config` if the user's prompt explicitly asks to expose operations to workflows, or a UI extension / workflow in the app needs a specific endpoint. See [Expose Operations to Workflows](#expose-operations-to-workflows) below.

**Claude Code safety net:** The plugin hook runs `adapt_spec_for_foundry.py` automatically if step 2 is missed. Do not assume that protection exists in Codex, Copilot CLI, Cursor, or other assistants.

## Authentication Configuration

| Type | OpenAPI `securitySchemes` Pattern | Install UI Prompt | Production Example |
|------|----------------------------------|-------------------|--------------------|
| **API Key (custom header)** | `type: apiKey`, `name: x-apikey` | API key field | VirusTotal |
| **API Key (Authorization header)** | `type: apiKey`, `name: Authorization`, `in: header`, `bearerFormat: SSWS` | API key field with prefix | Okta |
| **HTTP Bearer** | `type: http`, `scheme: bearer`, `bearerFormat: apikey` | Bearer token field | Anomali ThreatStream |
| **HTTP Basic** | `type: http`, `scheme: basic` | Username + Password fields | ServiceNow |
| **HTTP Basic (custom labels)** | `type: http`, `scheme: basic` + `x-cs-username-label` / `x-cs-password-label` | Custom-labeled fields | Workday |
| **OAuth 2.0 Client Credentials** | `type: oauth2` with `clientCredentials` flow | Client ID + Secret fields | SailPoint, CrowdStrike |
| **Dual Auth** | Multiple schemes defined | User chooses at install time | ServiceNow ITSM (basic + oauth2) |
| **CrowdStrike auto-auth** | Not needed — automatic for CrowdStrike APIs | None | — |

**API key prefix:** `apiKey` type with `name: Authorization` and `in: header` works for APIs that send tokens via the Authorization header. Add `bearerFormat` to specify the prefix (e.g., `SSWS`, `Bearer`, `Token`) — Foundry reads this field to populate the "API key parameter prefix" in the install UI. The adapt script infers the prefix from the scheme's description automatically.

For full vendor-specific auth examples, see [references/auth-examples.md](references/auth-examples.md).

## Adapting Specs for Foundry

### Server URL Configuration

Use a **fixed base URL** when the API domain is the same for all users:

```json
"servers": [{"url": "https://www.virustotal.com"}]
```

When the domain **varies per customer**, use a single server variable for the full domain. The Falcon console handles the protocol separately, so the URL must not include `https://`. The variable needs only a `description` — no `default`, no `enum`:

```json
"servers": [{"url": "{yourDomain}", "variables": {"yourDomain": {"description": "the \"yourDomain\" variable is replaced with a dynamic value at execution time"}}}]
```

Use a single variable for the complete domain. Splitting into `{subdomain}.vendor.com` causes certificate errors when users enter the full domain (e.g., `dev-12345.okta.com.okta.com`). A `default` value without `enum` renders a dropdown instead of a free-text input.

### Expose Operations to Workflows

> **Skip this unless the user asks for it.** Most API integrations work without `x-cs-operation-config`. Only add it when the prompt explicitly mentions sharing operations with Falcon Fusion SOAR workflows, or when a UI extension or workflow in the app needs a specific endpoint.

Add `x-cs-operation-config` to the specific operations requested:

```yaml
paths:
  /api/v1/users:
    get:
      operationId: listUsers
      x-cs-operation-config:
        workflow:
          name: listUsers
          description: List all users
          expose_to_workflow: true
          system: false
      summary: List all users
```

The `workflow` nesting under `x-cs-operation-config` is required. A flat `expose_to_workflow: true` directly under `x-cs-operation-config` will not work and causes deploy failures.

For autocomplete dropdown patterns and the HTTP Actions vs. Functions decision framework, see [references/spec-adaptation-examples.md](references/spec-adaptation-examples.md).

## Calling API Integrations

UI extensions call API integrations through Foundry-JS (sandboxed iframes block arbitrary HTTP requests). This pattern is from [foundry-sample-foundryjs-demo](https://github.com/CrowdStrike/foundry-sample-foundryjs-demo):

```javascript
import FalconApi from '@crowdstrike/foundry-js';
const falcon = new FalconApi();
await falcon.connect();

// Create integration instance
const apiIntegration = falcon.apiIntegration({
  definitionId: 'Okta',        // Matches API integration name in manifest.yml
  operationId: 'listUsers'     // Matches operationId in the OpenAPI spec
});

// Execute — use params.path, params.query, json (not body), or headers
const response = await apiIntegration.execute({
  request: {
    params: { query: { limit: 10 } }
  }
});

// Check for errors first
if (response.errors?.length > 0) {
  console.error('Request failed:', response.errors[0].message);
}

const statusCode = response.resources?.[0]?.status_code;
const body = response.resources?.[0]?.response_body;
```

For Python (FalconPy), Go (gofalcon), and detailed UI examples, see [references/calling-patterns.md](references/calling-patterns.md).

## Efficiency Rules

**Target: complete an API integration in under 5 minutes.** Download, adapt, register, deploy. That's it.

**Do NOT analyze, debug, or second-guess the spec's auth scheme.** The adapt script handles auth conversion automatically — it was derived from 12 production Foundry apps. Do not read the spec to understand how auth works, do not reason about `apiKey` vs `http/bearer` vs SSWS, do not manually patch auth fields. Just run the adapt script and register. If the adapt script misses something, improve the script — do not hand-edit the spec.

**NEVER use Read or sed on large spec files.** Vendor specs can be 10K-80K+ lines. Reading them into context wastes millions of tokens and slows everything down. Instead:

```bash
# Find a specific operationId
grep -n 'operationId: listUsers' /tmp/VendorApi.yaml

# Add x-cs-operation-config to a specific operation (cross-platform)
python3 -c "
import json, sys
spec = json.load(open(sys.argv[1]))
for path in spec.get('paths', {}).values():
    for op in path.values():
        if isinstance(op, dict) and op.get('operationId') == 'listUsers':
            op['x-cs-operation-config'] = {'workflow': {'name': 'listUsers', 'description': 'List all users', 'expose_to_workflow': True, 'system': False}}
json.dump(spec, open(sys.argv[1], 'w'), indent=2)
" /tmp/VendorApi.json
```

**Cross-platform note:** Use `python3` for spec manipulation instead of `sed` — it works on macOS, Linux, and Windows without syntax differences.

**Don't add what wasn't asked for.** If the prompt says "create an API integration for Okta," download the spec, adapt it, register it, and deploy. Don't read the spec to discover operations, don't add `x-cs-operation-config`, don't lint or trim. Foundry handles large specs fine.

## Common Pitfalls

- **Reading large spec files into context.** NEVER use Read or sed on vendor specs. They can be tens of thousands of lines. Use `grep` to find line numbers, `python3` to patch specific operations.
- **Manually analyzing or fixing auth schemes.** Trust the adapt script. Do not read the spec to reason about apiKey vs http/bearer vs SSWS. The adapt script handles auth conversion automatically. If it gets something wrong, improve the script.
- **Adding `x-cs-operation-config` when not asked.** Skip it unless the user's prompt explicitly mentions workflows or a UI/workflow needs a specific endpoint. Most integrations work without it.
- **Writing specs from scratch** when the vendor publishes one. Hand-written specs miss edge cases.
- **Running spec linters before importing.** Foundry's import handles vendor specs with lint errors. Linting wastes time and tempts trimming.
- **Trimming vendor specs.** Keep the full spec. Foundry handles large specs and unused operations gracefully.
- **Skipping `adapt_spec_for_foundry.py`.** Only Claude Code plugin installs run the automatic hook. Other assistants must invoke the bundled helper. The script converts unsupported auth schemes and fixes server URLs that would otherwise block saving in the Falcon console.
- **Including `https://` in server URLs** with variables. The Falcon console adds the protocol separately.
- **Adding `default` to server variables** for dynamic domains. This renders a dropdown instead of a text field.
- **Splitting domains** into `{subdomain}.vendor.com` instead of `{yourDomain}` for the full domain.
- **Using `oauth2 authorizationCode`** flow. Foundry only supports `clientCredentials`. The adapt script removes it automatically.

## Reading Guide

| Task | Reference |
|------|-----------|
| Download commands, structural fixes, server URL examples | [references/spec-adaptation-examples.md](references/spec-adaptation-examples.md) |
| Vendor-specific auth examples | [references/auth-examples.md](references/auth-examples.md) |
| Python/Go/UI calling patterns | [references/calling-patterns.md](references/calling-patterns.md) |

## Use Cases

For real-world implementation patterns, see:
- `use-cases/http-actions.md` — HTTP Request actions vs API integrations
- `use-cases/greynoise-deep-dive.md` — End-to-end third-party API app
- `use-cases/custom-soar-actions.md` — Custom Falcon Fusion SOAR actions

## Reference Implementations

- **[foundry-sample-foundryjs-demo](https://github.com/CrowdStrike/foundry-sample-foundryjs-demo)**: JSONPlaceholder (no auth, static URL, `x-cs-operation-config`)
- **[foundry-sample-logscale](https://github.com/CrowdStrike/foundry-sample-logscale)**: VirusTotal (`type: apiKey`, static URL)
- **[foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream)**: Anomali (`type: http`/`scheme: bearer`, dynamic URL)
- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: ServiceNow (`type: http`/`scheme: basic`, dynamic URL)
- **[foundry-sample-insider-risk-workday](https://github.com/CrowdStrike/foundry-sample-insider-risk-workday)**: Workday (`type: http`/`scheme: basic` + custom labels)
- **[foundry-sample-insider-risk-sailpoint](https://github.com/CrowdStrike/foundry-sample-insider-risk-sailpoint)**: SailPoint (`type: oauth2`/`clientCredentials`)
- **[foundry-sample-servicenow-itsm](https://github.com/CrowdStrike/foundry-sample-servicenow-itsm)**: ServiceNow ITSM (dual auth: basic + oauth2)
- **[foundry-sample-openrouter-toolkit](https://github.com/CrowdStrike/foundry-sample-openrouter-toolkit)**: OpenRouter (`type: apiKey`, static URL)
- See also: [Build API Integrations with Falcon Fusion SOAR HTTP Actions](https://www.crowdstrike.com/tech-hub/ng-siem/build-api-integrations-with-falcon-fusion-soar-http-actions/) and [Technical Deep Dive with GreyNoise](https://www.crowdstrike.com/tech-hub/ng-siem/technical-deep-dive-with-greynoise-building-a-falcon-foundry-app-for-crowdstrike-falcon-next-gen-siem/)

Referenced files: 4

collections-development14.2 KB

View saved version →

---
name: collections-development
description: Design JSON Schema collections and CRUD patterns for Falcon Foundry apps. TRIGGER when user asks to "create a collection", "define a JSON schema", "store data in Foundry", runs `foundry collections create`, or needs help with indexable fields, FQL queries, or collection access patterns. DO NOT TRIGGER for workflow YAML, function handlers, or UI components — use the appropriate sub-skill.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, collections, json-schema, nosql]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: data
---

# Foundry Collections Development

> **SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry data modeling specialist**.
>
> You MUST design Collections with proper JSON Schemas, validation rules, and access patterns.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

Falcon Foundry Collections are NoSQL document stores with JSON Schema validation. They provide persistent storage for app data with CRUD operations, FQL queries, and schema enforcement.

## Collection Naming Constraints

| Constraint | Rule |
|-----------|------|
| Length | 5-200 characters |
| Start/end | Must begin and end with a letter or number |
| Special characters | Only underscores (`_`) allowed — no hyphens, spaces, or other chars |
| Case | Case-sensitive |

## Collection Description Constraints

| Constraint | Rule |
|-----------|------|
| Length | 3-500 characters |
| Start | Must begin with an alphanumeric character |
| Allowed characters | Letters, numbers, spaces, dashes, periods, parentheses, and underscores only |
| Not allowed | Commas, colons, semicolons, quotes, slashes, or other special characters |

## Collection Limits

| Resource | Limit |
|----------|-------|
| Single object size | ~50 MB |
| Schema size | 256 KB |
| Object key length | 1-1,000 characters |
| Indexed fields per schema | 10 |
| Objects per collection | No enforced limit |
| Collections per app | No enforced limit |
| Search results per page | 500 max (default 50) |

## JSON Schema Requirements

- **JSON Schema draft 7 only** — newer drafts (`draft/2020-12`, `draft/2019-09`) fail validation
- Schema is auto-versioned: v1.0 on creation, auto-incremented on modification
- `additionalProperties: false` recommended — extra fields leak internal data and break type safety
- `x-cs-indexable: true` on individual properties for searchable fields (max 10 per collection)

## CLI Scaffolding

```bash
# Write schema to /tmp/ first — the CLI copies it into collections/
foundry collections create \
  --name "my_collection" \
  --schema /tmp/schema.json \
  --description "App data store" \
  --no-prompt \
  --wf-expose \
  --wf-tags "tag1,tag2"
```

This creates the collection directory, copies the schema, and updates `manifest.yml`. Edit the project copy at `collections/my_collection.json` afterward to refine.

## Collection API Access

Collections are managed via the CrowdStrike API or the `foundry-js` SDK. There are no CLI commands for reading/writing collection data, and collections can only be deleted from the Falcon Foundry UI (not the CLI).

```
PUT    /customobjects/v1/collections/{collection_name}/objects/{key}  — Create/update object
GET    /customobjects/v1/collections/{collection_name}/objects/{key}  — Get object by key
DELETE /customobjects/v1/collections/{collection_name}/objects/{key}  — Delete object
POST   /customobjects/v1/collections/{collection_name}/objects        — Search objects (FQL filter)
```

## JSON Schema Patterns

### Basic Schema

```json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "title": "Incident",
  "description": "Security incident record",
  "required": ["id", "title", "severity", "status", "created_at"],
  "additionalProperties": false,
  "properties": {
    "id": { "type": "string", "format": "uuid" },
    "title": { "type": "string", "minLength": 1, "maxLength": 200 },
    "severity": { "type": "integer", "minimum": 1, "maximum": 10 },
    "status": {
      "type": "string",
      "enum": ["open", "investigating", "contained", "resolved", "closed"]
    },
    "tags": {
      "type": "array",
      "items": { "type": "string", "maxLength": 50 },
      "maxItems": 20,
      "uniqueItems": true
    },
    "created_at": { "type": "string", "format": "date-time" }
  }
}
```

### Indexable Fields

Make fields searchable via FQL by marking them indexable. Two patterns are supported:

**Pattern A: Top-level array (preferred — used by most foundry-sample repos)**

```json
{
  "$schema": "https://json-schema.org/draft-07/schema",
  "x-cs-indexable-fields": [
    { "field": "/status", "type": "string", "fql_name": "status" },
    { "field": "/severity", "type": "integer", "fql_name": "severity" },
    { "field": "/created_at", "type": "string", "fql_name": "created_at" }
  ],
  "type": "object",
  "properties": {
    "status": { "type": "string" },
    "severity": { "type": "integer" },
    "created_at": { "type": "string", "format": "date-time" }
  }
}
```

**Pattern B: Per-field annotation**

```json
{
  "properties": {
    "compositeId": { "type": "string", "x-cs-indexable": true },
    "content": { "type": "string" }
  }
}
```

Both patterns work. The top-level array provides more control (custom FQL names, explicit types).

### Manifest Configuration

```yaml
# manifest.yml
collections:
  - name: incidents
    description: Security incident records
    schema: collections/incidents.json
    permissions: []
    workflow_integration:
      system_action: true
      tags:
        - Collection

  - name: audit_logs
    description: Audit log entries
    schema: collections/audit_logs.json
    permissions: []
    workflow_integration:
      system_action: false
      tags: []
```

Indexing is controlled entirely by `x-cs-indexable-fields` or `x-cs-indexable: true` in the JSON schema files, not in the manifest.

## CRUD Operations (TypeScript)

```typescript
import { Collection } from '@crowdstrike/foundry-js';

export class IncidentCollection {
  private collection: Collection<Incident>;

  constructor() {
    this.collection = new Collection<Incident>('incidents');
  }

  async create(data: Omit<Incident, 'id' | 'created_at' | 'updated_at'>): Promise<Incident> {
    const incident: Incident = {
      ...data,
      id: crypto.randomUUID(),
      created_at: new Date().toISOString(),
      updated_at: new Date().toISOString(),
    };
    await this.collection.create(incident.id, incident);
    return incident;
  }

  async get(id: string): Promise<Incident | null> {
    try {
      return await this.collection.get(id);
    } catch (error) {
      if (error.code === 'NOT_FOUND') return null;
      throw error;
    }
  }

  async update(id: string, updates: Partial<Incident>): Promise<Incident> {
    const existing = await this.get(id);
    if (!existing) throw new Error(`Incident ${id} not found`);
    const updated: Incident = {
      ...existing,
      ...updates,
      id: existing.id,
      created_at: existing.created_at,
      updated_at: new Date().toISOString(),
    };
    await this.collection.update(id, updated);
    return updated;
  }

  async delete(id: string): Promise<void> {
    await this.collection.delete(id);
  }

  async list(options?: { status?: string; limit?: number; offset?: number }) {
    const filters: Record<string, any> = {};
    if (options?.status) filters.status = options.status;
    return this.collection.query(filters, {
      limit: options?.limit ?? 50,
      offset: options?.offset ?? 0,
      sort: [{ field: 'created_at', order: 'desc' }],
    });
  }
}
```

## CRUD Operations (Python — from Functions)

Use `CustomStorage` (Service Class) to access collections from Python functions. Service classes are preferred over the Uber class (`APIHarnessV2`) because the Falcon Foundry functions editor auto-detects OAuth scopes from `from falconpy import CustomStorage`. See the `functions-development` skill's `references/python-patterns.md` for a complete handler example with Uber class alternative.

```python
import json
import os
from falconpy import CustomStorage

def _app_headers() -> dict:
    app_id = os.environ.get("APP_ID")
    if app_id:
        return {"X-CS-APP-ID": app_id}
    return {}

client = CustomStorage(ext_headers=_app_headers())

# Create or update (PutObject = upsert). Pass body as a dict.
client.PutObject(collection_name="incidents", object_key="incident-123",
                 body={"id": "incident-123", "title": "Suspicious process", "severity": 7})

# Read — GetObject returns bytes on success, dict on error
response = client.GetObject(collection_name="incidents", object_key="incident-123")
# In production, check isinstance(response, bytes) before decoding — see python-patterns.md for full error handling
incident = json.loads(response.decode("utf-8"))

# Delete
client.DeleteObject(collection_name="incidents", object_key="incident-123")

# Search (FQL filter — only indexed fields)
response = client.SearchObjects(collection_name="incidents",
                                filter="status:'open'+severity:>=5", limit=50)
# SearchObjects returns metadata — follow up with GetObject per key for full objects
for item in response.get("body", {}).get("resources", []):
    obj = client.GetObject(collection_name="incidents", object_key=item["object_key"])
    data = json.loads(obj.decode("utf-8"))
```

Key points:
- `CustomStorage(ext_headers=_app_headers())` applies `X-CS-APP-ID` to all requests (needed for local dev; Foundry sets it automatically in production)
- `PutObject` acts as upsert (creates or overwrites by key). Pass body as a dict.
- `GetObject` returns bytes directly — decode with `json.loads(response.decode("utf-8"))`
- `SearchObjects` returns metadata only, not full objects
- FQL filters only work on fields marked `x-cs-indexable: true` in the collection schema

## FQL Search Syntax

Only fields marked with `x-cs-indexable: true` can be used in FQL queries.

| Operation | Syntax | Example |
|-----------|--------|---------|
| Equality | `field:'value'` | `status:'open'` |
| Numeric comparison | `field:>=N` | `severity:>=5` |
| AND | `field1:'a'+field2:'b'` | `status:'open'+severity:>=5` |
| OR | `field:'a',field:'b'` | `status:'open',status:'investigating'` |
| Wildcard | `field:*'pattern'*` | `title:*'malware'*` |
| Sorting | `sort=field\|asc` | `sort=created_at\|desc` |

The `foundry-js` SDK's `search()` method accepts a `filter` parameter for FQL queries. The search endpoint is `POST /customobjects/v1/collections/{name}/objects` with a `filter` field in the request body.

## Workflow Share Settings

To make a collection accessible from workflows:

```yaml
collections:
  - name: incidents
    schema: collections/incidents/schema.json
    permissions: []
    workflow_integration:
      system_action: true           # true = app workflows only, false = also available as Fusion SOAR action
      tags:
        - Collection
```

| Setting | Behavior |
|---------|----------|
| `workflow_integration.system_action: true` | Available to app workflows only |
| `workflow_integration.system_action: false` | Available to both app workflows AND Falcon Fusion SOAR |

## RBAC and Direct API Access

Collections can be accessed directly via the CrowdStrike API (outside of functions) using custom roles with specific collection permissions. Include the `X-CS-APP-ID` header to identify your Foundry app. Foundry CLI credentials cannot access collections directly; use a separate API client with `Custom Storage` read/write scope.

## Common Pitfalls

- **Using `APIHarnessV2` (Uber class) for collection operations.** Use `CustomStorage` service class instead — the Foundry functions editor auto-detects OAuth scopes from service class imports but cannot parse Uber class `.command()` calls.
- **Using JSON Schema newer than draft 7.** Foundry only supports draft 7.
- **Missing indexes.** Fields used in queries must be marked with `x-cs-indexable: true` or listed in `x-cs-indexable-fields`. Max 10 per collection.
- **Invalid collection names.** Names must be 5-200 chars, start/end with letter or number, and contain only letters, numbers, and underscores.
- **Not configuring workflow share settings.** Set `workflow_integration.system_action: true` for app-only workflow access, or `false` to also expose collections as Falcon Fusion SOAR actions.
- **Trying to delete collections via CLI.** Collections can only be deleted from the Falcon Foundry UI.
- **Trying to manage objects via CLI.** Collection CRUD requires the CrowdStrike API or `foundry-js` SDK.
- **Schema field names must match exactly.** If a field name in your write payload doesn't match the collection schema (e.g., writing `score` when the schema defines `severity`), the write fails and returns errors in the response body — but the SDK does not throw. Without checking `result.errors`, the failure is invisible. Always read the collection schema file before writing seed data; verify required fields, enum values, and exact field names.
- **Not checking write responses for errors.** The SDK does not throw on server-side validation failures. Always check `result?.errors?.length` after write operations — errors include specific messages like `"missing property 'severity'"` or `"value must be one of 'low', 'medium', 'high', 'critical'"`. Verify persistence with a follow-up read or list call.

## Reading Guide

| Task | Reference |
|------|-----------|
| Migrations, testing, pagination, extended schemas, counter-rationalizations | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Use Cases

For real-world implementation patterns, see:
- `use-cases/collections.md` — CRUD operations, search, field types
- `use-cases/lookup-table-enrichment.md` — 3rd-party data for automated enrichment

## Reference Implementations

- **[foundry-sample-collections-toolkit](https://github.com/CrowdStrike/foundry-sample-collections-toolkit)**: CSV import, bulk operations, pagination workflows, test data generation. See also [Getting Started with Falcon Foundry Collections](https://www.crowdstrike.com/tech-hub/ng-siem/getting-started-with-falcon-foundry-collections-your-guide-to-structured-data-storage-in-foundry-apps/).

Referenced files: 1

debugging-workflows16.5 KB

View saved version →

---
name: debugging-workflows
description: Systematic troubleshooting for Falcon Foundry CLI errors, manifest validation failures, deploy failures, artifact runtime errors, and development server issues. TRIGGER when user encounters CLI errors, `foundry ui run` not working, deploy failures, authentication issues, function execution failures, "debug my function", "why did this fail", or any unexpected behavior during Foundry app development. Also trigger for headless/CI environment setup failures.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, debugging, cli, deployment, artifacts, functions, logs, execution]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: troubleshooting
---

# Foundry Debugging Workflows

Systematic procedures for diagnosing and resolving common CrowdStrike Falcon Foundry development issues.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

## Quick Diagnosis

```
What's happening?

CLI command hangs
├── In headless/CI environment → Missing --no-prompt or required flags (see Headless section)
└── In interactive terminal   → Check network/auth with foundry profile active

Deploy fails
├── Validation error → Check manifest YAML syntax, then deploy again
├── "Unknown error"  → Duplicate workflow name across apps in tenant
└── Silent failure   → Tenant may be missing required module (SKU) for requested scopes

foundry ui run fails
├── On new app              → Deploy backend capabilities first (API integrations, functions, collections resolve from cloud)
├── Permission errors       → Check manifest OAuth scopes, restart server, verify auth
└── Blank page / CORS error → noAttr() or base path removed from vite.config.js (see ui-development)

Function execution fails
├── Status 500 + "Server Error"  → Check function logs: foundry functions logs <exec_id>
├── Status 202 + no result       → Async execution: foundry functions exec status <exec_id>
├── "authorization failed"       → Missing custom-apps:write scope on API client
├── "artifact is not deployed"   → Deploy first: foundry apps deploy --no-prompt
├── No logs available            → Wait ~5 min, then: foundry functions logs <exec_id> --refresh
└── Unexpected response          → Read logs + source, correlate timestamps against handler code

Auth fails
├── 401/403 from API   → Check OAuth scopes in manifest
├── Login hangs        → Headless environment, no browser — use env vars or profile create --no-prompt
└── Works locally, fails in CI → Set FOUNDRY_API_CLIENT_ID env vars in CI config
```

## Local Testing

### Function Testing

```bash
# Via Foundry CLI with Docker (random ports, closest to production)
foundry functions run --name my-function

# Direct Go execution (port 8081, no Docker)
cd functions/my-function && go run main.go

# Direct Python execution (port 8081, no Docker)
cd functions/my-function && python3 main.py
curl -X POST http://localhost:8081/api/process -d '{"key":"value"}'

# With configuration file (local only)
CS_FN_CONFIG_PATH=./config.json python3 main.py
```

### Function Execution & Log Retrieval (Deployed Functions)

> **Requires Foundry CLI 2.1.0+.** These commands do not exist in CLI 2.0.x.

For testing against the deployed Lambda (not local Docker), use the execution commands:

```bash
# Execute a deployed function handler
foundry functions exec --handler my_handler '{"key": "value"}' --no-prompt

# Execute and wait for logs
foundry functions exec --handler my_handler --logs '{"payload": "data"}' --no-prompt

# Check status of an async (202) execution
foundry functions exec status <exec_id>

# Retrieve logs for a past execution
foundry functions logs <exec_id>

# List recent executions to find one to debug
foundry functions exec list --function my-fn --no-prompt
```

### Function Runtime Debugging

When a deployed function returns unexpected results or errors:

**Step 1: Execute and observe**
```bash
foundry functions exec --handler <handler> --logs '{"test": "input"}' --no-prompt
```

Note the exec_id and status code from the output.

**Step 2: Retrieve logs if not already displayed**
```bash
foundry functions logs <exec_id>
```

Logs arrive ~5 minutes after execution via the Firehose pipeline. The CLI polls automatically with a countdown.

**Step 3: Correlate logs against source**

The function source lives at the path in `manifest.yml`:
```yaml
functions:
  - name: my-function
    path: functions/my-function
    handlers:
      - name: my_handler
        method: POST
        api_path: /api/process
```

Read the handler source and compare against log timestamps and error messages.

**Step 4: Common runtime failures**

| Log Pattern | Likely Cause | Fix |
|-------------|-------------|-----|
| `ImportError: No module named X` | Missing from `requirements.txt` | Add dependency, redeploy |
| `KeyError: 'field'` | Missing field in request body | Add input validation |
| `401 Unauthorized` from FalconPy | Missing OAuth scope in manifest | Add scope, redeploy |
| `Timeout` / no logs appear | Function exceeded `max_exec_duration_seconds` | Increase timeout or optimize |
| Status 202, no result | Async execution | Poll: `foundry functions exec status <exec_id>` |
| Logs say "available" but empty | Logs not yet in pipeline | Wait 5 min or use `--refresh` |

**Step 5: Fix, redeploy, verify**
```bash
# After fixing the code:
foundry apps deploy --change-type Patch --change-log "fix handler error" --no-prompt

# Re-execute to verify
foundry functions exec --handler <handler> --logs '{"test": "input"}' --no-prompt
```

### RTR Script Testing

RTR scripts can only be tested via the CLI (not the Falcon console):

```bash
foundry rtr-scripts run --name my-script
```

Platforms: Windows (`script.ps1`), Linux (`script.sh`), macOS (`script.zsh`). Script size limit ~40KB. Deletion requires Falcon Administrator role in Falcon console UI.

### Workflow Mock Testing

```bash
foundry workflows triggers view --mock
foundry workflows actions view --mock
foundry workflows executions validate --mocks mymocks.json
foundry workflows executions start --definition my-workflow --mocks mymocks.json
foundry workflows executions view <execution_id>
```

### Deployment Diagnostics

Deployment is two-phase: validation (checks manifest and schemas) then artifact build. Use `foundry apps validate --no-prompt` to dry-run the validation phase after adding API integrations or collections (catches spec/schema issues in seconds). Don't validate right before deploy — deploy runs the same validation plus workflow semantics and name uniqueness checks.

## CLI Troubleshooting

### Step 1: Environment Validation

```bash
foundry version         # Check CLI version
foundry profile list    # Check available profiles
foundry profile active  # Verify active profile
```

### Step 2: Authentication

```bash
foundry login                                    # Re-authenticate via browser (interactive)
foundry profile delete --name <name> --no-prompt # Reset corrupted profile
foundry login                                    # Re-authenticate
```

### `connection issue` on Tenant Commands (Sandboxed Agents)

**Check the token cache write first — it is the usual cause and it is not a network problem.**

The CLI keeps its short-lived access token in `~/.config/foundry/token.json`, separate from the credentials in `configuration.yml`. The token lasts about 30 minutes, and the next tenant command after it expires writes a fresh one back to that file. That write is expected and is the safer design: a rotating 30-minute token beats a long-lived one, and keeping it out of the workspace means it never gets committed.

Assistants that confine writes to the workspace are denied that write, and the CLI reports only `connection issue` — the symptom, not the cause. Since a session usually begins with an already-expired token, this appears within the first few seconds, not after half an hour.

Fix: **request write access to `~/.config/foundry/` and retry the same command.** Say why — the CLI is refreshing its own token cache — and treat it as a routine low-risk escalation rather than a blocker. Do not ask the user to relaunch their assistant, and do not rewrite the profile, redirect the config path into the workspace, or copy credentials; those hide working credentials instead of granting the one needed write.

If the write succeeds and tenant commands still fail, then treat it as network: ask the user to run the command in their own terminal, and if it works there, request unsandboxed network access and retry once.

### Manifest Validation

Use `foundry apps validate --no-prompt` to validate the manifest and schemas without deploying. For OpenAPI specs, use `npx @redocly/cli lint` to validate structure locally.

If deploy fails with validation errors:
1. Check the error message — validation errors appear first
2. Comment out capabilities one by one to isolate the issue
3. Fix and re-validate incrementally

## Headless / Non-Interactive Environments

This is the most common failure mode when Foundry CLI is driven by agents (Claude Code) or CI/CD pipelines. Most commands default to interactive mode, which blocks indefinitely.

### `foundry login` Hangs or Fails

`foundry login` opens a browser for OAuth. In headless environments, use one of these alternatives:

**Option 1: Environment variables** (no login needed):
```bash
export FOUNDRY_API_CLIENT_ID="<client-id>"
export FOUNDRY_API_CLIENT_SECRET="<client-secret>"
export FOUNDRY_CID="<customer-id>"
export FOUNDRY_CLOUD_REGION="us-1"
```

**Option 2: Non-interactive profile creation**:
```bash
foundry profile create \
  --name "ci-profile" \
  --api-client-id "<id>" \
  --api-client-secret "<secret>" \
  --cid "<cid>" \
  --cloud-region "us-1" \
  --no-prompt
foundry profile activate --name "ci-profile"
```

**Option 3: Pre-populated config file** at `~/.config/foundry/configuration.yml`:
```yaml
profiles:
- name: ci-profile
  cloud_region: us-1
  credentials:
    cid: <customer-id>
    api_client_id: <client-id>
    api_client_secret: <client-secret>
active_profile: ci-profile
```

### Command Hangs Waiting for Input

Add `--no-prompt` to prevent interactive prompts. Nearly all commands support it: `apps create`, `apps validate`, `apps deploy`, `apps release`, `apps delete` (also needs `--force-delete`), `functions create`, `collections create`, `ui pages create`, `ui extensions create`, `rtr-scripts create`, `profile create`, `profile delete`, `workflows create`, and `api-integrations create`. Provide all required flags explicitly — run `foundry <command> --help` to identify them.

### Auth Works Locally but Fails in CI

The CI environment has no `~/.config/foundry/configuration.yml`. Set environment variables in CI pipeline configuration — they override local config.

## Common Issue Patterns

| Symptom | Likely Cause | First Action |
|---------|--------------|--------------|
| `foundry login` hangs | Headless environment | Use env vars or `profile create --no-prompt` |
| Any command hangs | Missing `--no-prompt` or required flags | Add flags, run `--help` |
| Deploy hangs indefinitely | Manifest validation issue | Check YAML syntax, deploy again |
| `foundry ui run` fails on new app | Backend not deployed | Run `foundry apps deploy` first |
| API calls return 403 | Insufficient OAuth scopes | Review manifest oauth section |
| Deploy fails silently | Tenant missing required module (SKU) | Verify tenant has Falcon module for scopes |
| Local server won't start | Port conflicts | Use `--port` flag or kill existing processes |
| Auth works locally, fails in CI | No config file in CI | Set `FOUNDRY_API_CLIENT_ID` env vars |
| `connection issue` in a sandboxed agent | Denied write to the CLI's token cache | Grant write to `~/.config/foundry/`; the token refresh is expected |
| Tenant command works for user, not agent | Agent network sandbox | Request elevation, then retry once |
| Page 404 after deploy/release | App not installed from App Catalog | Install from catalog, wait for propagation |
| Page 404 on new cloud only | Cloud-specific IDs in manifest | Strip IDs with yq before deploying to new cloud |
| Blank page, no CORS errors | Vite `root` changed from `src` | Restore `root: 'src'` in vite.config.js |
| Blank page with CORS errors | `noAttr()` removed from vite.config.js | Restore the `noAttr()` plugin in vite.config.js |
| Blank page, no errors in console | `falcon.connect()` not awaited | The platform iframe stays blank until the postMessage handshake completes — add `await falcon.connect()` before any rendering |
| Data not appearing after writes | Schema mismatch or missing error check | Verify field names/enums match schema exactly; check `result?.errors?.length` after writes |
| Dialog white background in dark mode | Shoelace panel defaults | Override `--sl-panel-background-color` with `var(--ground-floor)` |
| App install fails with no detail | Workflow CEL expression error | Test API integration in console, then inspect workflow editor for errors |

## Debugging App Install Failures

When a Foundry app fails to install with no useful error message, isolate the problem by testing each component in the Falcon console:

1. **Test the API integration first** — In the Falcon console, use the credentials from the install config to test the operation directly (e.g., run `listUsers` with the Okta domain and API key). This proves whether the spec and credentials work independently of the app.

2. **Eliminate unlikely suspects** — Static UI files (extensions, pages) don't cause install failures. If the API integration works, the problem is almost certainly in a workflow.

3. **Inspect the workflow in the console** — Open Falcon Fusion SOAR, edit the workflow, and look at each action's configuration. The workflow editor shows validation errors (like unknown variable references in CEL expressions) that the install API doesn't surface.

> **Example:** Apps failed to install with no detail. API integration tested fine. Editing the workflow in the console revealed "unknown variable" on the Print data action — the CEL variable path was missing the `Custom_` prefix the platform adds to all API integration names. The install error gave no hint; the workflow editor showed it immediately.

## Visual Debugging with Screenshots

Claude Code can read images directly. When the Falcon console shows something unexpected — a blank page, an error modal, a disabled button — a screenshot is often the fastest way to diagnose the issue.

**Without Playwright MCP (fastest):** Ask the user to take a screenshot of what they're seeing and paste or drag it into the conversation. Claude reads it immediately and can identify error messages, missing elements, wrong page states, or styling issues without any setup.

**With Playwright MCP:** If Playwright MCP is configured (`claude mcp add playwright -- npx @playwright/mcp@latest`), Claude can take screenshots directly via `browser_take_screenshot`. This is useful for interactive debugging sessions. See `e2e-testing/references/debugging-with-mcp.md` for details.

**From test failure artifacts:** When e2e tests fail, Playwright saves screenshots to `test-results/`. Read the `.png` file directly — it shows the exact page state at the moment of failure.

Screenshots are particularly effective for:
- Blank pages after deploy (missing iframe, broken Vite config)
- Extension buttons that don't appear or expand
- Error banners or modals with messages not surfaced by the CLI
- `foundry ui run` rendering issues (dark mode, missing Shoelace styles)
- App install dialogs with unexpected form fields

## Recovery Strategies

### Profile Corruption
1. Delete corrupted profile: `foundry profile delete --name <name> --no-prompt`
2. Re-authenticate with `foundry login`
3. Validate with test deployment

### Development Server
1. Kill existing processes: `pkill -f "foundry ui"`
2. Clear node_modules and reinstall
3. Restart with clean environment

### Manifest Issues
1. Backup current `manifest.yml`
2. Start with minimal working manifest
3. Incrementally add capabilities back, deploying after each addition

## Pre-Escalation Checklist

Before seeking external help:

- [ ] Verified CLI version with `foundry version`
- [ ] Checked authentication with `foundry profile active`
- [ ] Validated OpenAPI specs with `npx @redocly/cli lint` (if applicable)
- [ ] Tested with minimal configuration
- [ ] Reviewed CLI error messages
- [ ] Attempted recovery procedures
- [ ] Documented reproduction steps
development-workflow19.4 KB

View saved version →

---
name: development-workflow
description: Orchestrates the complete Falcon Foundry app lifecycle from requirements through deployment. TRIGGER when user asks to "create a Foundry app", "build a Foundry app", "plan a Foundry app", runs any `foundry apps` CLI command, or discusses Foundry app architecture. DO NOT TRIGGER when user is working on a specific capability (UI, function, workflow, collection) within an existing app — use the appropriate sub-skill instead. This skill OWNS the entire Foundry development flow. Do not delegate Foundry app creation to superpowers:brainstorming or superpowers:writing-plans — those skills do not know about the Foundry CLI.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, lifecycle, cli, deployment]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: orchestration
---

# Foundry Development Workflow

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry app lifecycle orchestrator**.
>
> **THIS SKILL OWNS THE FOUNDRY DEVELOPMENT FLOW.**
>
> **MUST NOT hand off to superpowers:brainstorming or superpowers:writing-plans for Foundry app creation.**
> Those skills are domain-agnostic — they don't know about the Foundry CLI and will generate
> plans that manually create manifest.yml and boilerplate files. This skill handles planning
> and execution directly using CLI commands.
>
> **IMMEDIATE ACTIONS REQUIRED:**
> 1. Follow the **App Creation Flow** below to go from user prompt → running app
> 2. Use `foundry apps create` and related CLI commands for ALL scaffolding
> 3. Delegate capability-specific content to Foundry sub-skills
> 4. Hand-write ONLY what the CLI cannot generate (OpenAPI content, workflow logic, UI code)
>
> **CRITICAL: add `--no-prompt` to every command that accepts it** — without it, interactive prompts cause `Error: EOF`. The `create`, `validate`, `deploy`, `release`, and `delete` commands all accept it (`apps delete` also needs `--force-delete`). Three reject it and fail with `unknown flag`: `foundry version`, `apps list`, and `apps list-deployments`. Verify with `foundry <command> --help`. When a command fails, MUST NOT fall back to `mkdir` — fix the command and retry.
>
> **CRITICAL: All `foundry` app commands MUST run from the app root directory** (where `manifest.yml` lives). The CLI resolves manifest paths relative to `os.Getwd()`, not relative to the manifest's location. Running `foundry apps validate`, `foundry apps deploy`, or `foundry ui run` from a subdirectory (e.g., `ui/extensions/my-ext/`) causes doubled paths and misleading "file not found" errors. After `cd`-ing into a subdirectory for `npm install && npm run build`, always `cd` back to the app root before running any `foundry apps *` or `foundry ui *` command. Commands that work from anywhere: `foundry version`, `foundry profile *`, `foundry apps list`.
>
> **Superpowers skills MAY supplement** (TDD discipline, code review) but MUST NOT replace this workflow.

This skill coordinates the full Falcon Foundry app lifecycle — from parsing requirements through scaffolding, implementation, and deployment. It delegates capability-specific work to sub-skills that know the platform details.

## Decision Tree

```
What does the user need?

Create a new Foundry app
└── Follow the App Creation Flow below

Add a capability to an existing app
├── API integration       → api-integrations
├── Workflow              → workflows-development
├── UI page/extension     → ui-development
├── Function              → functions-development
├── Collection            → collections-development
└── Falcon API from funcs → functions-falcon-api

Execute / test a deployed function
├── "run my function", "execute function"     → functions-development
├── "check execution status", "exec status"   → functions-development
├── "get function logs", "show logs"          → functions-development
├── "list executions"                         → functions-development
├── "test my function" (function tests.yml)   → functions-development
└── "run tests", "test cases"                 → functions-development

Debug a function failure
├── "debug my function", "why did it fail"    → debugging-workflows
├── "function returning errors"               → debugging-workflows
└── "no logs available"                       → debugging-workflows

Implement a known pattern (pagination, enrichment, ingestion, etc.)
└── Search use-cases/*.md for matching pattern → load for context

Debug / troubleshoot      → debugging-workflows
Security review           → security-patterns
E2E testing / Playwright  → e2e-testing

Standalone Fusion workflow (no app — trigger + existing actions only)
└── fusion-redirect (declines and points to the Falcon Fusion plugin)
```

### Routing When Sub-Skills Are Not Registered

Sub-skills live beside this one as `../<name>/SKILL.md`. On a single-entry-point install only this skill is discoverable, so capability-level requests land here — that is intended, not a mis-route. Read the sub-skill file from disk before writing any capability content, then:

- **`manifest.yml` already present** — skip Steps 1-2 and Step 4. Run the Step 3 prerequisite check, add the capability with its Step 5 CLI command, and follow Manifest Coordination.
- **No app yet** — run the full App Creation Flow.

This never overrides the fusion-redirect skill: a standalone Fusion workflow request is still a redirect, not a capability to add.


## App Creation Flow

### Step 1: Parse Requirements

Map user requests to Foundry capabilities:

| User Says | Capability | CLI Command |
|-----------|-----------|-------------|
| "API integration", "connect to X API" | API Integration | `foundry api-integrations create` |
| "workflow", "on-demand", "automate" | Workflow | `foundry workflows create` |
| "UI", "page", "dashboard" | UI Page | `foundry ui pages create` |
| "extension", "sidebar", "widget" | UI Extension | `foundry ui extensions create` |
| "function", "serverless", "backend" | Function | `foundry functions create` |
| "store data", "collection", "database" | Collection | `foundry collections create` |
| "run function", "execute", "test handler" | Function Execution | `foundry functions exec` |
| "get logs", "show execution logs" | Log Retrieval | `foundry functions logs` |
| "check status", "execution result" | Execution Status | `foundry functions exec status` |
| "test function", "run tests", "test cases" | Function Testing | `foundry functions test` |
| "debug function", "why did it fail" | Function Debugging | → debugging-workflows skill |

> **⚠️ "Summarize/list alerts, detections, or incidents" (a population the workflow doesn't already have) implies a source-of-truth fetch.** A request like "a workflow that emails a summary of high-severity alerts" needs the *set* of alerts, which is not reliably in NG-SIEM (Event Query can silently return nothing — repo contents are connector-dependent). Fetch it from the source of truth: a native platform action (e.g. Cases → Search Cases) first, or a FalconPy `Alerts`/`Detects` function when none fits — so the app needs BOTH that capability (plan the `alerts:read` scope) AND a workflow to schedule it and send email. **Exception:** *enriching* a detection the workflow was already triggered on (query by its ID) stays an Event Query and needs no function. See the `functions-falcon-api` skill and the `workflows-development` skill's `references/event-query-vs-api.md`.

### Step 1b: Check for Known Patterns

Before scaffolding, check if the user's request matches a known use case. Glob `use-cases/*.md` and scan the `description` field in each file's frontmatter. If a match is found, read the use case file for implementation context (architecture, capability order, gotchas) before proceeding.

Use cases cover common scenarios like API pagination, detection enrichment, lookup table creation, LogScale data ingestion, SOAR custom actions, and more. See `use-cases/README.md` for the full catalog.

### Step 2: Confirm App Name and Capabilities

**Always confirm the app name with the user before creating anything.** Use the assistant's available user-input mechanism; if none exists, ask directly in chat. Derive a reasonable default from the user's request (e.g., "okta-integration" for an Okta API integration), then present it as the recommended option with 1-2 alternatives. Include a brief description of what will be created.

**Page vs Extension disambiguation:** When the user mentions "UI" without specifying "page" or "extension", ask which they want using the available user-input mechanism. Offer two options: "Page" (standalone full-page view — dashboards, lists, management UIs) and "Extension" (sidebar widget embedded in detection/host/incident pages). Default to Page when running non-interactively (e.g., agent batch mode or test automation) since pages are the more common case.

For other decisions, prefer reasonable defaults: use React for UI, download public OpenAPI specs from vendor GitHub repos. Only ask additional clarifying questions when the prompt is genuinely ambiguous and a wrong guess would produce an unusable app.

### Step 3: CLI Prerequisite Check

```bash
foundry version          # Verify CLI installed
foundry profile active   # Verify authentication
foundry apps list        # Check existing apps (avoid name collisions)
```

If a tenant command fails with only `connection issue`, the usual cause is a
sandbox denying the CLI's token-cache write to `~/.config/foundry/` — an
expected refresh, not a network fault. Request write access to that directory
and retry; see
[headless operation](references/headless-operation.md). Never redirect the
config path into the workspace or copy credentials.

### Step 4: Scaffold the App

**Prerequisite:** User must have confirmed the app name in Step 2. Do not run this without confirmation.

Choose the location automatically: use an existing app when `manifest.yml` is
present; otherwise create in the current directory, or beside it when the
current directory is an unrelated repository. Request sandbox write access when
needed; never put the app inside an unrelated repository as a fallback.

```bash
foundry apps create --name "app-name" --description "description" --no-prompt --no-git
cd app-name
```

`--no-prompt` prevents interactive prompts that fail in non-interactive environments with `Error: EOF`. `--no-git` skips git initialization. The command is `foundry apps create` (there is no `init` command). If it fails, fix the command and retry — MUST NOT fall back to `mkdir`, which produces invalid manifest structure.

### Step 5: Add Capabilities (CLI Commands)

Run in dependency order. Write spec/schema files to `/tmp/` — the CLI copies them into the project and updates `manifest.yml` with generated IDs.

```bash
# 1. API integrations — delegate spec work to api-integrations sub-skill
#    IMPORTANT: Download specs inline with gh/curl. Do NOT spawn Explore agents for spec download.
foundry api-integrations create --name "MyApi" --description "desc" --spec /tmp/MyApi.yaml --no-prompt

# 2. Collections (names: letters, numbers, underscores ONLY)
foundry collections create --name "my_col" --schema /tmp/my_schema.json --description "desc" --no-prompt

# 3. VALIDATE EARLY — fail fast if specs or schemas are bad
foundry apps validate --no-prompt
# If validation fails, STOP. Fix the spec/schema — do not build UI on a broken backend.
# The adapt script should handle spec issues. If it didn't, improve the script.

# 4. Functions
foundry functions create --name "my-fn" --language python --description "desc" \
  --handler-name process --handler-method POST --handler-path /api/process --no-prompt

# 5. Workflows — MUST load workflows-development sub-skill before writing the spec file
foundry workflows create --name "My Workflow" --spec /tmp/My_workflow.yml --no-prompt

# 6. UI pages (standalone full-page views)
foundry ui pages create --name "my-page" --description "desc" --from-template React --homepage --no-prompt
foundry ui navigation add --name "My Page" --path / --ref pages.my-page

# 6b. UI extensions (sidebar widgets embedded in detection/host/incident pages)
# Run `foundry ui extensions list-sockets` to see available socket IDs
foundry ui extensions create --name "my-ext" --description "desc" --from-template React --sockets "activity.detections.details" --no-prompt
```

**Fail fast:** Validate right after API integrations and collections. `foundry apps validate` is a dry-run of deploy validation — it checks specs and schemas in seconds without building artifacts. It does NOT check workflow semantics or app name uniqueness (those are only checked on deploy). Don't validate right before deploy — deploy runs the same validation plus more. Don't manually fix spec issues — improve `adapt_spec_for_foundry.py` instead.

### Step 6: Write Domain-Specific Content

The CLI scaffolds structure but cannot generate app logic. Delegate to sub-skills:

- **OpenAPI spec** → api-integrations
- **Workflow YAML** → workflows-development
- **UI components** → ui-development
- **Function handlers** → functions-development
- **Collection schemas** → collections-development

> **⚠️ MANDATORY: Load the relevant sub-skill BEFORE writing any domain-specific code.** Without the sub-skill loaded, you WILL hallucinate incorrect formats and nonexistent APIs. Known failure modes:
>
> | Writing... | MUST load | Hallucination without it |
> |---|---|---|
> | Workflow YAML | `workflows-development` | Invented `definition/node_types/sdk_type` format instead of correct `trigger` + `actions` with `version_constraint` |
> | Function code calling Falcon APIs | `functions-falcon-api` | Invented `request.falcon_client.api_request(url='/foundry/entities/...')` instead of FalconPy SDK classes (`from falconpy import Hosts`) |
> | Function code calling a third-party API (Slack, Jira, PagerDuty, etc.) | `functions-falcon-api` + check `use-cases/` | Invented `falcon.command("createNotification")` or raw HTTP calls instead of `APIIntegrations().execute_command(definition_id="...", operation_id="...")`. The app MUST have an API integration (OpenAPI spec) for the service, then call it from the function via FalconPy `APIIntegrations` class. See foundry-sample-functions-python for reference. |
> | Function code accessing collections | `collections-development` | Invented REST endpoints for collection CRUD instead of FalconPy `CustomStorage` service class |
>
> ALWAYS load the sub-skill first. This is not optional.

### Step 7: Final Build and Deploy

```bash
# Build UI (required before deploy) — MUST cd back to app root afterward
cd ui/pages/my-page && npm install && npm run build && cd ../../..
# For extensions: cd ui/extensions/my-ext && npm install && npm run build && cd ../../..

# IMPORTANT: Verify you are in the app root (where manifest.yml lives) before running
# foundry apps/ui commands. The CLI resolves paths relative to cwd, not the manifest location.

# Final deploy (run ONCE, never re-deploy to check status)
foundry apps deploy --no-prompt --change-type Patch --change-log "Complete app"

# Poll deployment status — run immediately, do NOT prepend sleep
foundry apps list-deployments
# If still in progress, wait 5s then poll again:
# sleep 5 && foundry apps list-deployments

# Local UI development (deploy first if UI calls backend capabilities)
foundry ui run
```

**Deploy once, poll with `list-deployments`.** Running `deploy` multiple times creates duplicate deployments and wastes minutes.

```bash
# Release (run ONCE after deploy succeeds)
foundry apps release --change-type Patch --deployment-id <id> --notes "Release notes"
```

**Note:** There is no `list-releases` command. After `release`, check status via the App Manager URL printed in the output, or wait ~30s and proceed to testing.

`foundry ui run` only serves UI locally — backend capabilities (API integrations, functions, collections) resolve from the cloud. Deploy those first.

## Multi-Cloud Deployment

To deploy the same app to multiple clouds (US-1, US-2, US-3, EU-1, etc.):

1. **Strip all IDs** before deploying to a new cloud — IDs are cloud-specific:
   ```bash
   yq -i 'del(.. | select(has("id")).id) | del(.. | select(has("app_id")).app_id)' manifest.yml
   ```
   This DELETES the keys entirely. Setting them to empty/null is NOT the same and will cause errors.

2. **Switch profile** to the target cloud:
   ```bash
   foundry profile activate --name "eu-1-profile"
   ```

3. **Deploy and release** as normal.

4. **Install from App Catalog** — after releasing on a new cloud, the app must be explicitly installed from the Falcon console App Catalog. It does NOT auto-install.

5. **Wait for propagation** — installation may take several minutes before the page URL becomes accessible. A 404 on `/api2/ui-extensions/entities/pages/v1` immediately after install is normal; retry after a few minutes.

**Important:** Back up your manifest before stripping IDs if you want to preserve the original cloud's IDs: `cp manifest.yml manifest.yml.backup`

## Existing App Workflow

When `manifest.yml` already exists, work is primarily editing existing files. Use CLI only for:
- `foundry apps run` / `foundry ui run` — local development
- `foundry apps deploy` / `foundry apps release` — deployment
- `foundry api-integrations create` etc. — adding new capabilities

## Manifest Coordination

**Dependency order:** Collections → Functions → Workflows → UI (each may depend on the previous)

- **MUST NOT edit manifest.yml** unless a deploy fails with "app name already exists" (rename only). The CLI sets `path`, `entrypoint`, scopes, and IDs correctly — manual edits cause double-path errors and wasted deploy cycles.
- **MUST NOT edit vite.config.js** — the React blueprint is turnkey. Do not change `base`, `root`, or `noAttr()`. Just edit React/JS component code and deploy.
- OAuth scopes are auto-managed for CLI-created artifacts — MUST NOT manually add `api-integrations:read`
- Use `npx @redocly/cli lint` for OpenAPI validation (not Python/Ruby YAML parsers)
- Validate early with `foundry apps validate --no-prompt` after adding API integrations and collections — but don't validate right before deploy (deploy runs the same checks plus more)

## Reading Guide

| Task | Reference |
|------|-----------|
| Headless/CI setup, env vars, US-GOV-1 | [references/headless-operation.md](references/headless-operation.md) |
| Superpowers plugin coordination | [references/superpowers-integration.md](references/superpowers-integration.md) |
| Token management, performance targets | [references/performance-optimization.md](references/performance-optimization.md) |
| Counter-rationalizations, red flags | [references/counter-rationalizations.md](references/counter-rationalizations.md) |
| Lifecycle phases, manifest patterns, CLI state, app operations, local e2e runs | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Improving These Skills

If a skill gave incorrect guidance, was missing a pattern, or required extra trial-and-error to get right, the user can ask you to capture the fix at the end of the session:

```
What did you learn from this session that could improve the Foundry skills?
Clone https://github.com/CrowdStrike/foundry-skills.git,
create a branch, update the skills with this knowledge, and
create a PR on GitHub.
```

Steps Claude will handle: create a branch, update the relevant `skills/*/SKILL.md`, and create a PR.

This turns a one-session fix into a permanent improvement for all users.

Referenced files: 6

e2e-testing20.8 KB

View saved version →

---
name: e2e-testing
description: End-to-end testing for Falcon Foundry apps using Playwright and @crowdstrike/foundry-playwright. TRIGGER when user asks to "add e2e tests", "add playwright tests", "write end-to-end tests", "test my app", or mentions "e2e", "playwright", or "end-to-end" in the context of testing a Foundry app. DO NOT TRIGGER during normal app creation, UI development, or function development. This skill is opt-in; not all apps need e2e tests.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, e2e, playwright, testing]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: testing
---

# Foundry E2E Testing

End-to-end testing for Falcon Foundry apps using [Playwright](https://playwright.dev/) and the [`@crowdstrike/foundry-playwright`](https://github.com/CrowdStrike/foundry-playwright) library.

The library provides authentication, app install/uninstall, page objects, and configuration so each app only writes its app-specific tests.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

## Quick Start

### 1. Create the `e2e/` directory

```
my-foundry-app/
├── e2e/
│   ├── .env                    # Local credentials (git-ignored)
│   ├── .env.sample             # Template for other developers
│   ├── .gitignore
│   ├── package.json
│   ├── playwright.config.ts
│   └── tests/
│       └── foundry.spec.ts
├── manifest.yml
└── ...
```

### 2. `package.json`

Node.js LTS is recommended.

```json
{
  "name": "playwright-foundry",
  "version": "1.0.0",
  "scripts": {
    "test": "npx playwright test",
    "test:ui": "npx playwright test --ui",
    "test:debug": "npx playwright test --debug",
    "test:verbose": "DEBUG=true npx playwright test --reporter=list"
  },
  "type": "commonjs",
  "devDependencies": {
    "@crowdstrike/foundry-playwright": "0.5.0",
    "@types/node": "25.6.0"
  }
}
```

**Always pin exact versions** — never use `"latest"`, `"^"`, or `"~"`. Check npm for the current version of each package.

The library brings `@playwright/test`, `@dotenvx/dotenvx`, and `otpauth` as transitive dependencies. No need to install them separately.

### 3. `.env`

```sh
FALCON_USERNAME=your.email@company.com
FALCON_PASSWORD=your-password
FALCON_AUTH_SECRET=your-totp-secret
FALCON_BASE_URL=https://falcon.us-2.crowdstrike.com
APP_NAME=your-app-name
```

**Convention for sample apps:** Set `APP_NAME` to match the manifest `name` field, which should match the repo name (e.g., `foundry-sample-functions-python`). This avoids spaces in names and simplifies CI. This is a convention, not a hard requirement.

### 4. `playwright.config.ts`

```typescript
import { defineFoundryConfig } from '@crowdstrike/foundry-playwright';

export default defineFoundryConfig();
```

This gives you the standard 4-project pipeline automatically:
1. **setup**: authenticate and save session state
2. **app-install**: install the app via App Catalog
3. **chromium**: run your tests
4. **app-uninstall**: clean up after tests

### 5. `.gitignore`

```
node_modules/
playwright/.auth/
playwright-report/
test-results/
.env
```

### 6. Install and run

```bash
cd e2e
npm install
npx playwright install chromium --with-deps
npm test
```

## Writing Tests

### Available page objects

The library provides these page objects:

| Class | Purpose |
|-------|---------|
| `WorkflowsPage` | Search, open, execute, and verify Falcon Fusion SOAR workflows |
| `DetectionExtensionPage` | Navigate to Endpoint Detections, expand extensions, return iframe FrameLocator |
| `HostManagementPage` | Navigate to host management, retrieve host IDs |
| `AppCatalogPage` | Install, uninstall, and navigate to apps |
| `AppBuilderPage` | Disable workflow provisioning before install |
| `AppManagerPage` | Find and navigate to apps in App Manager |
| `FoundryHomePage` | Navigate to Falcon Foundry home |

### Fixtures pattern

Create `src/fixtures.ts` to wire up page objects as Playwright fixtures. **Only import what your tests actually use** — don't define unused fixtures:

```typescript
import { test as baseTest } from '@playwright/test';
import { DetectionExtensionPage, WorkflowsPage } from '@crowdstrike/foundry-playwright';

type FoundryFixtures = {
  detectionExtensionPage: DetectionExtensionPage;
  workflowsPage: WorkflowsPage;
};

export const test = baseTest.extend<FoundryFixtures>({
  detectionExtensionPage: async ({ page }, use) => { await use(new DetectionExtensionPage(page)); },
  workflowsPage: async ({ page }, use) => { await use(new WorkflowsPage(page)); },
});

export { expect } from '@playwright/test';
```

Playwright fixtures are lazy (only instantiated when a test requests them), so unused fixtures don't hurt performance — but they add confusion and dead code. Add fixtures as you add tests that need them.

### Example test: workflows

```typescript
import { test } from '../src/fixtures';

test.describe.configure({ mode: 'serial' });

test('should execute workflow', async ({ workflowsPage }) => {
  test.setTimeout(180000);
  await workflowsPage.navigateToWorkflows();
  await workflowsPage.executeAndVerifyWorkflow('My Workflow Name');
  await workflowsPage.verifyWorkflowExecutionCompleted();
});

test('should execute workflow with input', async ({ workflowsPage, hostManagementPage }) => {
  test.setTimeout(180000);
  const hostId = await hostManagementPage.getFirstHostId();
  if (!hostId) { test.skip(true, 'No hosts available'); return; }

  await workflowsPage.navigateToWorkflows();
  await workflowsPage.executeAndVerifyWorkflow('Host Details Workflow', {
    inputs: { 'Host ID': hostId },
  });
  await workflowsPage.verifyWorkflowExecutionCompleted();
});
```

`executeAndVerifyWorkflow()` handles search, execution trigger, and initial verification. `verifyWorkflowExecutionCompleted()` opens the execution detail view in a new tab and polls until the status leaves "In Progress" — it fails the test if the execution reports "Failed" and times out after 120s by default. For render-only checks (e.g., ServiceNow workflows without credentials), use `verifyWorkflowRenders()`.

### Example test: UI extensions

```typescript
import { test, expect } from '../src/fixtures';

test('should render extension', async ({ detectionExtensionPage }) => {
  const frame = await detectionExtensionPage.openExtension('hello');
  await expect(frame.getByText(/My App Title/i)).toBeVisible({ timeout: 10000 });
});
```

`openExtension()` navigates to Endpoint Detections, opens the first detection, scrolls to the named extension button, expands it, and returns the iframe FrameLocator.

## Apps with Configuration Screens

If your app has API integration settings during install (e.g., ServiceNow credentials), the default install will fail because the Install button stays disabled until fields are filled.

### 1. Add integration credentials to `.env` and `.env.sample`

```sh
# .env.sample — commit this as a template
SERVICENOW_INSTANCE_URL=https://dev123456.service-now.com
SERVICENOW_USERNAME=your-servicenow-username
SERVICENOW_PASSWORD=your-servicenow-password

# .env — local values, git-ignored
SERVICENOW_INSTANCE_URL=https://dev99999.service-now.com
SERVICENOW_USERNAME=admin
SERVICENOW_PASSWORD=s3cret
```

### 2. Create a custom `tests/app-install.setup.ts`

```typescript
import { test as setup } from '@playwright/test';
import { AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  const catalog = new AppCatalogPage(page);

  const instanceUrl = process.env.SERVICENOW_INSTANCE_URL;
  const username = process.env.SERVICENOW_USERNAME;
  const password = process.env.SERVICENOW_PASSWORD;
  if (!instanceUrl || !username || !password) {
    throw new Error('Missing required ServiceNow env vars: SERVICENOW_INSTANCE_URL, SERVICENOW_USERNAME, SERVICENOW_PASSWORD');
  }

  await catalog.installApp(config.appName, {
    configureSettings: async (page) => {
      await page.getByRole('textbox', { name: 'Name', exact: true }).fill('ServiceNow Integration');
      await page.getByRole('textbox', { name: 'Instance' }).fill(instanceUrl);
      await page.getByRole('textbox', { name: 'Username' }).fill(username);
      await page.getByRole('textbox', { name: 'Password' }).fill(password);
    },
  });
});
```

The library loads `.env` automatically (via `@dotenvx/dotenvx`) so `process.env` values are available without extra setup. In CI, set these as GitHub Actions secrets instead.

### 3. Point the config at the custom install

```typescript
export default defineFoundryConfig({
  appInstallDir: './tests',
});
```

**How to discover field names:** Use Playwright MCP to take a snapshot of the install page and inspect the form fields. See [debugging-with-mcp.md](references/debugging-with-mcp.md).

### Disabling Workflow Provisioning

Apps with workflows that require valid API credentials will fail during install if you provide fake credentials for the configuration screen. Some workflows are also long-running (e.g., Anomali ThreatStream ingestion) and shouldn't be provisioned during testing because they'll start automatically. Use `AppBuilderPage.disableWorkflowProvisioning()` before install to skip provisioning:

```typescript
import { test as setup } from '@playwright/test';
import { AppBuilderPage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  setup.setTimeout(300000);
  const appBuilder = new AppBuilderPage(page);
  await appBuilder.disableWorkflowProvisioning(config.appName);

  const catalog = new AppCatalogPage(page);
  await catalog.installApp(config.appName);
});
```

This navigates to the App Builder, finds the app, toggles off workflow provisioning for each workflow, and saves. Call it before `installApp()` in your custom `app-install.setup.ts`.

### Multi-Screen Configuration Wizards

Some apps have settings spread across multiple screens (e.g., Workday with 4 screens, SailPoint with 3). Navigate between screens using the "Next setting" button:

```typescript
import { test as setup } from '@playwright/test';
import { AppBuilderPage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

setup('install app', async ({ page }) => {
  setup.setTimeout(300000);
  const appBuilder = new AppBuilderPage(page);
  await appBuilder.disableWorkflowProvisioning(config.appName);

  const catalog = new AppCatalogPage(page);

  await catalog.installApp(config.appName, {
    configureSettings: async (page) => {
      const nextButton = page.getByRole('button', { name: 'Next setting' });

      // Screen 1: First API integration settings
      await page.getByRole('textbox', { name: 'Name' }).fill('My Integration');
      await page.getByRole('textbox', { name: 'Host' }).fill('https://api.example.com');
      await nextButton.click();
      await page.waitForLoadState('networkidle').catch(() => {});

      // Screen 2: Authentication settings
      await page.getByRole('textbox', { name: 'Client ID' }).fill(process.env.CLIENT_ID!);
      await page.getByRole('textbox', { name: 'Client Secret' }).fill(process.env.CLIENT_SECRET!);
      await nextButton.click();
      await page.waitForLoadState('networkidle').catch(() => {});

      // Screen 3: Additional settings (last screen — "Install app" button is visible here)
      await page.getByRole('textbox', { name: 'Tenant ID' }).fill(process.env.TENANT_ID!);
    },
  });
});
```

The `waitForLoadState('networkidle').catch(() => {})` after each "Next setting" click gives the next screen time to load. The `.catch(() => {})` prevents failures if the page is already idle.

## Custom Page Objects

For app-specific UI that the library doesn't cover, extend `BasePage`:

```typescript
import { Page, expect } from '@playwright/test';
import { BasePage, AppCatalogPage, config } from '@crowdstrike/foundry-playwright';

export class MyAppPage extends BasePage {
  constructor(page: Page) {
    super(page, 'MyAppPage'); // display name for logging only
  }

  protected getPagePath(): string {
    throw new Error('Direct path navigation not supported. Use navigateToInstalledApp() instead.');
  }

  protected async verifyPageLoaded(): Promise<void> {
    await this.page.locator('h1').filter({ hasText: /My App/i }).waitFor();
  }

  async navigateToInstalledApp(): Promise<void> {
    return this.withTiming(async () => {
      const catalog = new AppCatalogPage(this.page);
      await catalog.navigateToInstalledApp(config.appName);
      await this.verifyPageLoaded();
    }, 'Navigate to installed app');
  }

  async verifyAppContent(): Promise<void> {
    return this.withTiming(async () => {
      const iframe = this.page.frameLocator('iframe[name="portal"]');
      const heading = iframe.getByRole('heading', { name: /My App/i });
      await expect(heading).toBeVisible({ timeout: 10000 });
    }, 'Verify app content');
  }
}
```

Foundry app page URLs contain deployment-specific IDs that change on every deploy, so never hardcode paths. Use `AppCatalogPage.navigateToInstalledApp()` to navigate via the App Catalog menu instead.

`BasePage` gives you `smartClick()`, `withTiming()`, `navigateToPath()`, `elementExists()`, a `logger`, and a `waiter` (SmartWaiter instance).

Add the custom page to your fixtures alongside library page objects.

## Debugging with Playwright MCP

Playwright MCP is invaluable for writing and debugging e2e tests. See [references/debugging-with-mcp.md](references/debugging-with-mcp.md) for detailed patterns.

**Key principle:** When using Playwright MCP interactively, authentication is manual. Navigate to the Falcon login page, then tell the user: *"Please log in to Falcon in the browser, then let me know when you're ready."* Do not attempt automated TOTP login through MCP.

## CI with GitHub Actions

The sample apps use a shared `e2e.yml` workflow pattern. Rather than duplicating it here (where it would go stale), reference the canonical implementation:

**Reference:** [`foundry-sample-functions-python/.github/workflows/e2e.yml`](https://github.com/CrowdStrike/foundry-sample-functions-python/blob/main/.github/workflows/e2e.yml)

### Key CI concepts

1. **Concurrency serialization**: Only one e2e run per repo at a time (prevents deployment collisions)
2. **Unique app names**: CI generates a unique name from repo + actor + timestamp to avoid conflicts
3. **Manifest ID stripping**: `yq -i 'del(.. | select(has("id")).id) | del(.. | select(has("app_id")).app_id)' manifest.yml`
4. **Deploy → wait → release → test → cleanup**: The workflow deploys, polls for success, releases, runs tests, then always deletes the app
5. **App cleanup**: `foundry apps delete -f` runs in an `always()` step so apps are cleaned up even on failure

### Required GitHub secrets

| Secret | Purpose |
|--------|---------|
| `FOUNDRY_API_CLIENT_ID` | Foundry CLI authentication |
| `FOUNDRY_API_CLIENT_SECRET` | Foundry CLI authentication |
| `FOUNDRY_CID` | CrowdStrike Customer ID |
| `FOUNDRY_CLOUD_REGION` | Cloud region (us-1, us-2, us-3, eu-1) |
| `FALCON_USERNAME` | Falcon console login for Playwright |
| `FALCON_PASSWORD` | Falcon console password |
| `FALCON_AUTH_SECRET` | TOTP secret for 2FA |

### CI-specific behavior

The library detects `CI=true` and adjusts:
- Higher timeouts (60s default vs 45s local)
- Retries enabled (2 retries vs 0 local)
- `.env` loading skipped (credentials come from environment)

## `defineFoundryConfig()` Options

| Option | Type | Default |
|--------|------|---------|
| `testDir` | `string` | `'./tests'` |
| `appInstallDir` | `string` | Library built-in (override for apps with config screens) |
| `timeout` | `number` | 60s (CI) / 45s (local) |
| `retries` | `number` | 2 (CI) / 0 (local) |
| `reporter` | `string` | `'list'` |
| `use` | `object` | Merged with defaults (`testIdAttribute: 'data-test-selector'`) |
| `projects` | `array` | Replaces the default 4-project pipeline if provided |

### Sidebar navigation flakiness

The Falcon sidebar menu re-renders during navigation, which can detach DOM elements mid-click and cause intermittent timeouts. This affects any test that navigates through the sidebar (detections, workflows, host management, etc.). The library's navigation methods include retry logic for this, but with `retries: 0` locally, a single re-render failure will fail the test.

For apps that navigate through the sidebar, set `retries: 2` to handle this reliably:

```typescript
export default defineFoundryConfig({ retries: 2 });
```

## Common Pitfalls

| Problem | Cause | Fix |
|---------|-------|-----|
| "Could not find app in catalog" | `APP_NAME` doesn't match the manifest `name` | Ensure `.env` APP_NAME matches `manifest.yml` name field |
| Install button stays disabled | App has config screens (API integrations) | Create custom `app-install.setup.ts` with `configureSettings()` |
| "collection existed previously but was deleted" | Stale IDs in manifest from a previous deployment | Strip all IDs: `yq -i 'del(.. \| select(has("id")).id) \| del(.. \| select(has("app_id")).app_id)' manifest.yml` |
| Tests fail on first run after deploy | Release not propagated yet | Wait 15-30s after release before running tests; CI workflow includes a sleep |
| Tests pass locally but fail in CI | Different timeout/retry settings | Check `CI=true` is set; library auto-adjusts timeouts |
| Login fails with account lockout | Running `npm test` in multiple apps simultaneously | Run tests for one app at a time. Concurrent login attempts against the same Falcon account trigger rate-limiting and temporarily lock the account. |

## Reference Implementations

All [CrowdStrike/foundry-sample-*](https://github.com/CrowdStrike?q=foundry-sample) apps have e2e tests using this library:

| App | Highlights |
|-----|------------|
| [foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python/tree/main/e2e) | `configureSettings()`, workflows, extension, host details |
| [foundry-sample-mitre](https://github.com/CrowdStrike/foundry-sample-mitre/tree/main/e2e) | Custom page objects (`MitreChartPage`), parallel tests |
| [foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream/tree/main/e2e) | API integration config |
| [foundry-sample-category-blocking](https://github.com/CrowdStrike/foundry-sample-category-blocking/tree/main/e2e) | UI page testing |
| [foundry-sample-charlotte-toolkit](https://github.com/CrowdStrike/foundry-sample-charlotte-toolkit/tree/main/e2e) | Charlotte AI toolkit |
| [foundry-sample-collections-toolkit](https://github.com/CrowdStrike/foundry-sample-collections-toolkit/tree/main/e2e) | Collection CRUD |
| [foundry-sample-detection-translation](https://github.com/CrowdStrike/foundry-sample-detection-translation/tree/main/e2e) | Detection extension |
| [foundry-sample-foundryjs-demo](https://github.com/CrowdStrike/foundry-sample-foundryjs-demo/tree/main/e2e) | Foundry-JS demo |
| [foundry-sample-idp-notifications](https://github.com/CrowdStrike/foundry-sample-idp-notifications/tree/main/e2e) | IDP notifications |
| [foundry-sample-insider-risk-sailpoint](https://github.com/CrowdStrike/foundry-sample-insider-risk-sailpoint/tree/main/e2e) | SailPoint integration |
| [foundry-sample-insider-risk-workday](https://github.com/CrowdStrike/foundry-sample-insider-risk-workday/tree/main/e2e) | Workday integration |
| [foundry-sample-logscale](https://github.com/CrowdStrike/foundry-sample-logscale/tree/main/e2e) | LogScale data ingestion |
| [foundry-sample-ngsiem-importer](https://github.com/CrowdStrike/foundry-sample-ngsiem-importer/tree/main/e2e) | Next-Gen SIEM importer |
| [foundry-sample-openrouter-toolkit](https://github.com/CrowdStrike/foundry-sample-openrouter-toolkit/tree/main/e2e) | OpenRouter AI toolkit |
| [foundry-sample-rapid-response](https://github.com/CrowdStrike/foundry-sample-rapid-response/tree/main/e2e) | Rapid response |
| [foundry-sample-scalable-rtr](https://github.com/CrowdStrike/foundry-sample-scalable-rtr/tree/main/e2e) | Scalable RTR |
| [foundry-sample-servicenow-idp](https://github.com/CrowdStrike/foundry-sample-servicenow-idp/tree/main/e2e) | ServiceNow IDP |
| [foundry-sample-servicenow-itsm](https://github.com/CrowdStrike/foundry-sample-servicenow-itsm/tree/main/e2e) | ServiceNow ITSM |
| [foundry-sample-threat-intel](https://github.com/CrowdStrike/foundry-sample-threat-intel/tree/main/e2e) | Threat intelligence |
| [foundry-sample-zscaler-internet-access](https://github.com/CrowdStrike/foundry-sample-zscaler-internet-access/tree/main/e2e) | Zscaler integration |

## Integration with Other Skills

- **development-workflow**: E2E testing is delegated from the orchestrator when users request it
- **ui-development**: Tests verify UI pages and extensions render correctly in the Falcon console
- **workflows-development**: Tests verify workflow execution and completion
- **debugging-workflows**: Use for deploy/release issues that block testing

Referenced files: 1

functions-development19.6 KB

View saved version →

---
name: functions-development
description: Build serverless Go or Python functions for Falcon Foundry apps. TRIGGER when user asks to "create a function", "write a serverless function", "build backend logic", runs `foundry functions create`, or needs help with FDK handler patterns, function testing, or collection integration from functions. Also TRIGGER when user asks to "execute a function", "run my function", "debug this function", "get function logs", "check execution status", "write function tests", "test my function handler", or "add test cases for my function". DO NOT TRIGGER for generic "write integration tests" or "write tests" without function context — ask which capability they want to test first. DO NOT TRIGGER for calling Falcon platform APIs from functions — use functions-falcon-api instead. DO NOT TRIGGER for workflow YAML or UI components. DO NOT TRIGGER for Playwright/e2e/browser tests — use e2e-testing instead.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, functions, serverless, python, go, execution, debugging, logs, testing]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: backend
---

# Foundry Functions Development

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry serverless functions specialist**.
>
> **Loading this skill is NOT a command to do anything.** It only equips you with function-development knowledge. Do NOT run any `foundry functions` command (especially `exec`, `test`, or a deploy) just because the skill loaded — wait for the user's actual request, and if their intent isn't clear yet, ask what they'd like to do. Never execute, test, or deploy a function unprompted.
>
> When you DO implement or modify functions, you MUST use proper CrowdStrike SDK patterns, structured error handling, and Collection integration:
> 1. Use CrowdStrike SDKs (gofalcon/falconpy) for ALL API interactions
> 2. Implement structured JSON responses with proper status codes
> 3. Apply input validation before processing any request

Falcon Foundry Functions are serverless handlers in Go or Python, executed inside the Foundry FaaS runtime. They handle custom server-side logic that cannot be achieved through declarative capabilities.

## Functions as a Last Resort

Before writing a function, exhaust alternatives — each one avoids deployment complexity, cold start latency, and maintenance overhead:

- **Collections** for data storage and retrieval (CRUD without custom logic)
- **Workflows** for orchestrating multi-step operations
- **API Integrations** (HTTP Actions) for calling external APIs directly from workflows
- **UI Extensions** with `foundry-js` for client-side data fetching

## Credential Management — No Secrets System Exists

**There is no secrets management in Falcon Foundry.** When calling third-party REST APIs:

| Scenario | Approach | Credentials |
|----------|----------|-------------|
| Third-party REST API (VirusTotal, Slack, Jira, etc.) | API integration in manifest + `APIIntegrations().execute_command_proxy()` | Platform-managed at install time |
| CrowdStrike Falcon API | FalconPy zero-arg constructor (`Alerts()`, `Hosts()`) | Platform-managed automatically |
| Third-party GraphQL API (no OpenAPI spec) | Function with `requests` + env var | Visible in app exports (⚠️ security risk) |

**CRITICAL:** If the API has a REST endpoint and an OpenAPI spec exists, you MUST use an API integration. NEVER use `os.environ` with API keys, `requests.get()` with hardcoded URLs, or localStorage for credentials when an API integration can handle it. Raw HTTP with env vars technically works, but credentials are unencrypted and visible in app exports.

## Reference Files

This skill is split across multiple files. Consult these for full examples:

| Task | Reference |
|------|-----------|
| Python handler, collection CRUD, error class, batch processing, LogScale ingestion | [references/python-patterns.md](references/python-patterns.md) |
| Go FDK handler, Falcon client auth, collection CRUD, alerts handler | [references/go-patterns.md](references/go-patterns.md) |
| Falcon console testing (Python editor), function logs (viewing logs in UI and Advanced Event Search), Go/Python tests, local testing, Docker vs direct, config file patterns | [references/testing-patterns.md](references/testing-patterns.md) |

## Resource Limits

| Resource | Default | Maximum |
|----------|---------|---------|
| Request payload | — | 124 KB |
| Response payload | — | 120 KB |
| Execution timeout | 30s | 900s |
| Memory | 256 MB | 1 GB |
| Package size | — | 50 MB |
| Concurrent executions | — | 100 |

## Runtime Environment

**Python runtime version: 3.13** (manylinux_2_28, glibc 2.28). When choosing package versions for `requirements.txt`, ensure they have wheels compatible with this environment. Packages requiring `manylinux_2_17` (glibc 2.17) or `manylinux_2_28` (glibc 2.28) are compatible; those requiring newer glibc versions (e.g., `manylinux_2_39`) may fail at import time.

When linting Python functions with pylint, use `--py-version=3.13` or set `py-version=3.13` in `.pylintrc` to match the runtime.

## CLI Scaffolding

```bash
foundry functions create \
  --name "my-function" \
  --language python \
  --description "Process incoming data" \
  --handler-name process \
  --handler-method POST \
  --handler-path /api/process \
  --no-prompt
```

## Function Execution & Debugging

> **Requires Foundry CLI 2.1.0+.** The commands below (`foundry functions exec`, `test`, `logs`) do not exist in CLI 2.0.x. If the user's CLI is below 2.1.0, inform them and offer to upgrade: `brew upgrade crowdstrike/foundry-cli/foundry`.

> **⚠️ DEPLOY FIRST — `exec` and `test` run against the deployed Lambda, not local code.** Never deploy automatically; ask the user first.

For the full command reference, request-data confirmation workflow, tests.yml schema, and debugging steps, see [references/execution-and-testing.md](references/execution-and-testing.md).

Key commands:

```bash
foundry functions exec --handler <handler> '<json>' --no-prompt
foundry functions exec status <exec_id>
foundry functions logs <exec_id>
foundry functions test --no-prompt
```

## Reviewing Function Code

Before recommending a deploy, review the handler for logging adequacy, sensitive data, and schema coverage. See [references/code-review-checklist.md](references/code-review-checklist.md) for the full checklist.

## Function I/O Schemas — Required at Creation Time

> **⚠️ CRITICAL:** Functions called from workflows MUST be created with `--input-schema` and `--output-schema`. Schemas bind only at creation time. A function created without an output schema produces **no visible output** in its Fusion action — the action completes, but downstream steps cannot reference any data from it.

### Why This Matters

The platform registers a handler's schemas with the workflow engine when the function is created. Without a response schema:
- The Fusion action shows zero output fields
- Workflow references like `${data['my_function.output.field']}` resolve to nothing
- The workflow appears to run but produces empty results

Passing `--wf-expose` alone is not enough. It creates the workflow binding; the schema fields are still written as `null`.

### Create the Function with Schemas

Write the two JSON Schema files first, then pass them to `foundry functions create`. The CLI copies them into the function directory and records them in the manifest:

```bash
foundry functions create \
  --name "query-stats" \
  --language python \
  --description "Query execution statistics" \
  --handler-name query \
  --handler-method POST \
  --handler-path /api/query \
  --input-schema request_schema.json \
  --output-schema response_schema.json \
  --wf-expose \
  --no-prompt
```

The resulting manifest entry references the schemas **by filename on the handler** — not as inline JSON, and not under `workflow_integration`:

```yaml
functions:
    - name: query-stats
      path: functions/query-stats
      handlers:
        - name: query
          method: POST
          api_path: /api/query
          request_schema: request_schema.json
          response_schema: response_schema.json
          workflow_integration:
            id: <generated>
            disruptive: false
            system_action: true
      language: python
```

Without `--input-schema`/`--output-schema`, both fields are written as `null` — including when `--wf-expose` is set. `--wf-expose` creates the workflow binding but does NOT generate schemas.

### Fixing a Function Missing Schemas

Adding `request_schema`/`response_schema` to the manifest by hand and redeploying does NOT bind them. The binding happens only at creation time via the CLI flags. To fix a function that was created without them:

1. Remove the function's entry from `manifest.yml` and delete its directory
2. Recreate it with `--input-schema`/`--output-schema` as shown above
3. Update any workflow YAML referencing it — the `workflow_integration.id` changes
4. Redeploy

**Warning:** Step 3 matters. Recreating the function gives it a new workflow integration ID, so existing workflow YAML points at nothing. Edit the workflow YAML in place to reference the new ID — do NOT delete and recreate the *workflows* to force a refresh, which triggers `409 name must be unique for an app` and can corrupt the app's dependency graph. See the `workflows-development` skill for details.

## Language Comparison

| Feature | Go | Python |
|---------|-----|--------|
| HTTP Methods | GET, POST, PUT, DELETE | GET, POST, PUT, PATCH, DELETE |
| FDK Package | `github.com/CrowdStrike/foundry-fn-go` | `crowdstrike-foundry-function` |
| CrowdStrike SDK | gofalcon | falconpy |
| PATCH support | **No** | Yes |
| UI Editor support | No | Yes |

Use Go for performance-critical workloads, concurrency, and type safety. Use Python for rapid development, PATCH support, and UI Editor development.

## Manifest Structure

```yaml
functions:
  - name: gather-evidence
    description: "Collect evidence from multiple sources"
    language: python
    path: "functions/gather-evidence"
    environment_variables:
      FALCON_CLIENT_ID: "${secrets.falcon_client_id}"
      LOG_LEVEL: "info"
    max_exec_duration_seconds: 30
    max_exec_memory_mb: 128
    handlers:
      - name: process
        method: POST
        path: "/api/investigations/{id}/evidence"
      - name: healthcheck
        method: GET
        path: "/api/health"
```

Handler fields: `name` (identifier), `method` (HTTP verb), `path` (route, supports `{param}` placeholders). A single function can expose multiple HTTP endpoints. Function description max 100 characters (alphanumeric only).

## Go FDK Pattern

```go
package main

import (
    "context"
    "log/slog"
    fdk "github.com/CrowdStrike/foundry-fn-go"
)

type greetingReq struct {
    Name string `json:"name"`
}

func newHandler(_ context.Context, _ *slog.Logger, _ fdk.SkipCfg) fdk.Handler {
    m := fdk.NewMux()
    m.Post("/greetings", fdk.HandleFnOf(func(ctx context.Context, r fdk.RequestOf[greetingReq]) fdk.Response {
        return fdk.Response{
            Code: 200,
            Body: fdk.JSON(map[string]string{"greeting": "Hello, " + r.Body.Name}),
        }
    }))
    return m
}

func main() {
    fdk.Run(context.Background(), newHandler)
}
```

Key FDK concepts: `fdk.SkipCfg` (no config file), `fdk.NewMux()` (router), `fdk.HandleFnOf[T]` (typed handler), `fdk.RequestOf[T]` (typed request with `.Body`, `.Params`, `.URL`, `.Method`), `fdk.JSON()` (response body helper).

### Go Authentication

Go requires explicit credential wiring through the FDK. Use `fdk.FalconClientOpts()` for correct cloud and user-agent configuration:

```go
opts := fdk.FalconClientOpts()
falconClient, err := falcon.NewClient(&falcon.ApiConfig{
    AccessToken:       accessToken,
    Cloud:             falcon.Cloud(opts.Cloud),
    Context:           ctx,
    UserAgentOverride: opts.UserAgent,
})
```

## Python FDK Pattern

```python
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response

func = Function.instance()

@func.handler(method='POST', path='/greetings')
def on_post(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    name = request.body.get("name", "World")
    return Response(body={'greeting': f'Hello, {name}!'}, code=200)

@func.handler(method='GET', path='/health')
def on_get(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    return Response(body={'status': 'ok'}, code=200)

if __name__ == '__main__':
    func.run()
```

### Python Authentication

FalconPy handles credential discovery automatically. Call Service Class constructors with zero arguments:

```python
from falconpy import Alerts
falcon = Alerts()  # Auth is automatic — do not pass credentials
```

- **In Foundry cloud**: Uses context-based authentication (injected by the platform)
- **Locally**: Reads `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` from environment variables

FalconPy already reads env vars internally, so writing a `get_falcon_client()` wrapper that manually reads credentials adds no value and breaks context auth in the cloud.

### Calling Registered API Integrations from Functions

When your app has an API integration registered in `manifest.yml`, call it from functions using FalconPy's `APIIntegrations` class. Do NOT make raw HTTP calls (urllib/requests) to the third-party API — always go through the Foundry platform proxy:

```python
from falconpy import APIIntegrations

api = APIIntegrations()  # Zero-arg auth, same as other FalconPy classes

# Call using definition_id + operation_id from your manifest
response = api.execute_command_proxy(
    body={
        "resources": [
            {
                "definition_id": "ZscalerAPI",      # matches manifest api_integrations name
                "operation_id": "urlLookup",        # matches OpenAPI spec operationId
            }
        ]
    },
)
```

For APIs that need a request body or query parameters:

```python
response = api.execute_command_proxy(
    body={
        "resources": [
            {
                "definition_id": "Anomali API",
                "operation_id": "Intelligence",
                "request": {
                    "params": {
                        "query": {"type": "ip", "value": ip_address}
                    }
                },
            }
        ]
    },
)
```

**Why the proxy?** The platform manages OAuth tokens, rate limiting, and audit logging for registered integrations. Raw HTTP calls bypass all of this — while they can work with hardcoded or env-var credentials, those values are stored unencrypted and visible to anyone who exports the app.

**Local testing note:** When testing locally, you may need the UUID `definition_id` from `manifest.yml` (assigned by the platform). In production, the human-readable integration name (e.g., `"ZscalerAPI"`) works as the `definition_id` value.

Reference implementations:
- [foundry-sample-zscaler-internet-access](https://github.com/CrowdStrike/foundry-sample-zscaler-internet-access) (6 functions using `execute_command_proxy`)
- [foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream) (IOC ingestion via API integration)
- [foundry-sample-openrouter-toolkit](https://github.com/CrowdStrike/foundry-sample-openrouter-toolkit) (`execute_command` variant)

### requirements.txt Best Practices

Pin all dependencies to exact versions (`==`) for reproducible builds and supply chain safety. The one exception is `crowdstrike-falconpy`, which should be left unpinned so functions always pick up the latest SDK (needed for context-based auth and new service classes). Ensure the file ends with a trailing newline.

```
crowdstrike-foundry-function==1.1.4
crowdstrike-falconpy
```

## Workflow Array Output

When a function returns array data to a workflow, wrap the array in a JSON object. Direct array returns are not supported by the workflow engine:

```python
# Correct — workflows can reference $step.output.items
return Response(body={'items': [{'id': 1}, {'id': 2}]}, code=200)

# Breaks workflow variable resolution
return Response(body=[{'id': 1}, {'id': 2}], code=200)
```

## Error Handling

Return structured JSON with status codes. MUST NOT leak raw stack traces:

```python
try:
    result = process(request.body)
    return Response(body={"status": 200, "data": result}, code=200)
except ValueError as e:
    return Response(body={"error": {"code": "INVALID_INPUT", "message": str(e)}}, code=400)
except Exception:
    return Response(body={"error": {"code": "INTERNAL_ERROR", "message": "An unexpected error occurred"}}, code=500)
```

For the full `FunctionError` class with enum codes, see [references/python-patterns.md](references/python-patterns.md).

## Common Pitfalls

- **Using `requests` instead of CrowdStrike SDKs.** The SDKs handle auth, retries, regions, and error parsing.
- **Using `APIHarnessV2` (Uber class) for collection operations.** Use `CustomStorage` service class instead so the Foundry functions editor can auto-detect OAuth scopes. See the Collection CRUD Pattern in [references/python-patterns.md](references/python-patterns.md).
- **Manually reading env vars for FalconPy auth.** `Alerts()` with zero arguments handles all credential discovery.
- **Shared utility files across functions.** `sys.path.append("../")` works locally but not in Foundry's FaaS runtime. Copy shared files into each function directory.
- **`SearchObjects` returns metadata, not objects.** Follow up with `GetObject` to retrieve actual content. For bulk reads, use FQL filters to narrow the search rather than fetching all keys and reading them one by one in a loop.
- **Returning arrays directly to workflows.** Wrap in a JSON object (`{'items': [...]}` not `[...]`).
- **Using PATCH with Go functions.** Go only supports GET, POST, PUT, DELETE.
- **Using `definition_id` vs. name for API integrations.** When testing locally, use the UUID `definition_id` from `manifest.yml`. In production, the human-readable name (e.g., `"ZscalerAPI"`) works as the `definition_id` value. See the [Calling Registered API Integrations](#calling-registered-api-integrations-from-functions) section above.
- **Making raw HTTP calls to third-party APIs.** When an API integration is registered in the manifest, MUST use `APIIntegrations().execute_command_proxy()`. Raw urllib/requests calls can technically work with hardcoded credentials or env vars, but credentials are stored unencrypted and visible in app exports — a security risk. Always prefer the API integration path.

## Use Cases

For real-world implementation patterns, see:
- `use-cases/python-functions.md` — Python handler patterns, SDK usage, testing
- `use-cases/logscale-ingestion.md` — Ingesting custom data into Falcon LogScale
- `use-cases/api-pagination.md` — Pagination strategies in functions and workflows

## Reference Implementations

- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: Reference Python function patterns. See also [Dive into Falcon Foundry Functions with Python](https://www.crowdstrike.com/tech-hub/ng-siem/dive-into-falcon-foundry-functions-with-python/).
- **[foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream)**: Side-by-side Go and Python auth patterns.
- **[foundry-sample-logscale](https://github.com/CrowdStrike/foundry-sample-logscale)**: LogScale ingestion patterns.
- **[foundry-sample-servicenow-itsm](https://github.com/CrowdStrike/foundry-sample-servicenow-itsm)**: Go function patterns with ServiceNow integration.
- **[foundry-sample-ngsiem-importer](https://github.com/CrowdStrike/foundry-sample-ngsiem-importer)**: Python function for importing threat intel into NG-SIEM.

Referenced files: 5

functions-falcon-api20.5 KB

View saved version →

---
name: functions-falcon-api
description: Call CrowdStrike Falcon platform APIs (detections, alerts, hosts, RTR) from within Foundry function handlers. TRIGGER when user asks to "call Falcon APIs from a function", "use FalconPy in a function", "use gofalcon in a function", or needs to integrate Falcon platform APIs within serverless function code. DO NOT TRIGGER when user wants to expose external third-party APIs to Foundry — use api-integrations instead.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, functions, falcon-api, falconpy, gofalcon]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: backend
---

# Falcon API Integration in Functions

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Falcon API integration specialist for Foundry functions**.
>
> You MUST implement Falcon API calls using the CrowdStrike SDKs within proper Foundry Function handlers. Authentication is automatic when using the FDK handler pattern.
>
> The FalconPy `Detects` class is **removed**. Do not import it. Use `Alerts` for detection queries.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

This skill covers calling CrowdStrike Falcon APIs from within Foundry functions (serverless Go or Python code). Authentication is completely automatic when code runs inside Foundry function handlers — the platform handles all OAuth flows, token management, and credential injection.

For exposing external APIs to Foundry via OpenAPI specs, see **api-integrations** instead.

> **🚫 DEPRECATED API — NEVER USE:**
>
> **Do NOT import or use the `Detects` class from FalconPy.** The Detects API (`/detects/entities/detects/v2`) is deprecated and returns **405 Method Not Allowed**. Any code using `Detects()`, `query_detects()`, or `get_detect_summaries()` will fail at runtime.
>
> **Use instead:** `from falconpy import Alerts` with `query_alerts_v2()` / `get_alerts_v2()`. Filter by `product:'detections'` to scope to detections only.

## Reference Files

| Topic | Reference |
|-------|-----------|
| Retry decorator with exponential backoff, multi-API enrichment, counter-rationalizations table | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Python: Zero-Argument Authentication

FalconPy Service Classes require zero arguments when called inside Foundry Function handlers. The `crowdstrike.foundry.function` FDK provides the handler decorator that enables automatic authentication:

```python
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import Alerts, Hosts

func = Function.instance()

@func.handler(method='GET', path='/api/alerts')
def get_alerts(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    falcon = Alerts()  # Zero-arg constructor — auth is automatic

    limit = min(int(request.params.get("limit", 50)), 100)
    # FQL filter: high-severity alerts from the last 24 hours.
    # Combine conditions with '+' (AND); relative times like 'now-24h' are supported.
    response = falcon.query_alerts_v2(
        filter="severity_name:'High'+created_timestamp:>'now-24h'",
        limit=limit,
        sort="created_timestamp|desc",
    )

    if response["status_code"] != 200:
        logger.error(f"Failed to query alerts: {response.get('errors')}")
        return Response(body={"error": "Failed to fetch alerts"}, code=500)

    alert_ids = response.get("body", {}).get("resources", [])
    if not alert_ids:
        return Response(body={"alerts": []}, code=200)

    details_response = falcon.get_alerts_v2(ids=alert_ids)
    if details_response["status_code"] != 200:
        return Response(body={"error": "Failed to fetch alert details"}, code=500)

    alerts = details_response.get("body", {}).get("resources", [])
    return Response(body={"alerts": alerts}, code=200)

if __name__ == '__main__':
    func.run()
```

**How it works:**
- **In Foundry cloud**: Uses context-based authentication injected by the platform
- **Locally**: Reads `FALCON_CLIENT_ID` and `FALCON_CLIENT_SECRET` from environment variables

FalconPy already reads env vars internally, so writing a `get_falcon_client()` wrapper adds no value and breaks context auth in the cloud.

## Go: FDK Helper Authentication

Go requires the FDK helper to get cloud and user-agent configuration:

```go
package main

import (
    "context"
    "log/slog"
    "github.com/crowdstrike/gofalcon/falcon"
    "github.com/crowdstrike/gofalcon/falcon/client"
    fdk "github.com/crowdstrike/foundry-fn-go"
)

func newHandler(_ context.Context, _ *slog.Logger, _ fdk.SkipCfg) fdk.Handler {
    m := fdk.NewMux()

    m.Get("/api/alerts", fdk.HandleFnOf(func(ctx context.Context, r fdk.RequestOf[struct{}]) fdk.Response {
        accessToken := r.Header.Get("X-CS-ACCESSTOKEN")

        opts := fdk.FalconClientOpts()
        falconClient, err := falcon.NewClient(&falcon.ApiConfig{
            AccessToken:       accessToken,
            Cloud:             falcon.Cloud(opts.Cloud),
            Context:           ctx,
            UserAgentOverride: opts.UserAgent,
        })
        if err != nil {
            return fdk.Response{Code: 500, Body: fdk.JSON(map[string]string{"error": "Failed to authenticate"})}
        }

        // ... API calls with falconClient ...
        return fdk.Response{Code: 200, Body: fdk.JSON(map[string]interface{}{"alerts": []interface{}{}})}
    }))

    return m
}

func main() {
    fdk.Run(context.Background(), newHandler)
}
```

## Common API Patterns

### Detection Queries (via Alerts API)

> **⚠️ The legacy Detects API (`/detects/entities/detects/v2`) is deprecated and returns 405 Method Not Allowed.** Use the Alerts API (`/alerts/entities/alerts/v3`) for all detection queries — it covers both detections and cases.

```python
@func.handler(method='GET', path='/api/detections')
def get_detections(request: Request, config, logger) -> Response:
    falcon = Alerts()  # Zero-arg — auth is automatic

    severity_min = int(request.params.get("severity_min", 3))
    limit = min(int(request.params.get("limit", 50)), 100)

    # Use Alerts v2 methods — these hit /alerts/entities/alerts/v3 under the hood.
    # FQL filter: severity threshold + product "detections" (excludes cases/incidents).
    query_response = falcon.query_alerts_v2(
        filter=f"severity:>='{severity_min}'+product:'detections'",
        limit=limit,
        sort="created_timestamp|desc",
    )
    if query_response["status_code"] != 200:
        return Response(body={"error": "Failed to query detections"}, code=500)

    alert_ids = query_response.get("body", {}).get("resources", [])
    if not alert_ids:
        return Response(body={"detections": []}, code=200)

    details = falcon.get_alerts_v2(ids=alert_ids)
    if details["status_code"] != 200:
        return Response(body={"error": "Failed to get details"}, code=500)

    return Response(body={"detections": details["body"]["resources"]}, code=200)
```

### Host Lookups

```python
@func.handler(method='GET', path='/api/hosts/{hostname}')
def get_host_details(request: Request, config, logger) -> Response:
    falcon = Hosts()

    hostname = request.params.get("hostname")
    if not hostname:
        return Response(body={"error": "Hostname required"}, code=400)

    query = falcon.query_devices_by_filter(filter=f"hostname:'{hostname}'")
    if query["status_code"] != 200:
        return Response(body={"error": "Failed to query devices"}, code=500)

    host_ids = query.get("body", {}).get("resources", [])
    if not host_ids:
        return Response(body={"error": f"Host not found: {hostname}"}, code=404)

    details = falcon.get_device_details(ids=host_ids)
    host = details.get("body", {}).get("resources", [{}])[0]
    return Response(body={"host": host}, code=200)
```

### Multi-API Enrichment

Combining `Hosts` and `Alerts` in one handler follows the same query-then-get-details shape as above. See [references/advanced-patterns.md](references/advanced-patterns.md) for the full example.

## LogScale / NG-SIEM Queries from Functions

When you need to **query** LogScale data (e.g., workflow execution stats from the "fusion" repo, detection telemetry, custom ingested events), use the `NGSIEM` class. This is distinct from **ingestion** (covered in functions-development).

> **⚠️ Class Disambiguation:**
> - `NGSIEM` — Use for **querying** (async search jobs) and file uploads (lookup files, CSV imports). This is the query interface.
> - `FoundryLogScale` — Use only for **ingestion** (`ingest_data`). Despite the name suggesting broad LogScale functionality, it does NOT have the search methods.

### Query Pattern (Python)

```python
import time
from logging import Logger
from typing import Any, Dict, Union
from crowdstrike.foundry.function import Function, Request, Response
from falconpy import NGSIEM

func = Function.instance()

# Always "search-all" — see the gotcha below. Scope to a repo in the query, not here.
REPO = "search-all"


def run_logscale_query(ngsiem, query_string, start, end, logger, max_wait=40):
    """Execute a CQL query as an async search job and return result rows.

    ``start`` / ``end`` accept Humio relative strings ("24h", "7d", "30d",
    "now") or epoch-millisecond integers.
    """
    payload = {"queryString": query_string, "start": start, "end": end, "isLive": False}
    # Must be search=, not body= — see the keyword gotcha below.
    started = ngsiem.start_search(repository=REPO, search=payload)

    if not isinstance(started, dict) or started.get("status_code", 500) >= 300:
        logger.error(f"start_search failed: {started}")
        return None

    # start_search returns "resources"; get_search_status returns "body".
    job_id = (started.get("resources") or {}).get("id")
    if not job_id:
        logger.error(f"start_search returned no job id: {started}")
        return None

    # Poll until done
    waited = 0.0
    poll_interval = 1.5
    while waited < max_wait:
        time.sleep(poll_interval)
        waited += poll_interval
        status = ngsiem.get_search_status(repository=REPO, id=job_id)
        if not isinstance(status, dict) or status.get("status_code", 500) >= 300:
            logger.error(f"get_search_status failed: {status}")
            return None
        sbody = status.get("body") or {}
        if sbody.get("done"):
            return sbody.get("events", []) or []

    logger.warning(f"query timed out after {max_wait}s: {query_string}")
    return None


@func.handler(method='POST', path='/api/query')
def handle_query(request: Request, config: Union[Dict[str, Any], None], logger: Logger) -> Response:
    ngsiem = NGSIEM()  # Zero-arg auth — automatic in Foundry

    query = request.body.get("query", "")
    start = request.body.get("start", "24h")
    end = request.body.get("end", "now")

    events = run_logscale_query(ngsiem, query, start, end, logger)
    if events is None:
        return Response(body={"error": "LogScale query failed"}, code=500)

    return Response(body={"results": events, "count": len(events)}, code=200)

if __name__ == '__main__':
    func.run()
```

### The `search=` Keyword Gotcha

**Use `search=`.** It works on every FalconPy version. `body=` was silently ignored before 1.6.5 ([falconpy#1491](https://github.com/CrowdStrike/falconpy/issues/1491), fixed in [#1497](https://github.com/CrowdStrike/falconpy/pull/1497)). Since FalconPy is unpinned, `search=` is the safe default.

Response keys are asymmetric: `start_search` renames its payload to `resources` (read `started["resources"]["id"]`), while `get_search_status` does not (read `status["body"]`).

### The "search-all" Repository Gotcha

**CRITICAL:** Always pass `repository="search-all"` when querying from Foundry functions. Passing a specific repository name (e.g., `"fusion"`, `"main"`) causes **403 Forbidden** errors at runtime (`"scope not permitted"`), even if the repository exists and the app has `humio-auth-proxy` scopes granted.

The NG-SIEM queryjobs API is addressed by **searchable view**, not by raw repo name. Use `search-all` as the repository and add `#repo=fusion` (or whichever repo you need) as a filter prefix in your query string:

```python
# Filter to a specific repo within the query itself
query = "#repo=fusion | execution_log_type=summary | execution_log_subtype=end | groupby([status], function=count(execution_id, distinct=true))"
events = run_logscale_query(ngsiem, query, "24h", "now", logger)
```

### Required OAuth Scopes

```yaml
# manifest.yml
auth:
    scopes:
        - humio-auth-proxy:read     # Required for queries
        - humio-auth-proxy:write    # Required only if also uploading lookup files
```

### Reference

- [Exporting Falcon Next-Gen SIEM Query Results to CSV with Falcon Foundry](https://www.crowdstrike.com/tech-hub/ng-siem/exporting-falcon-next-gen-siem-query-results-to-csv-with-falcon-foundry/) — background on async LogScale querying from Foundry, plus CSV export. Note that this post reaches for `FoundryLogScale` with `mode="async"`; the `NGSIEM` pattern above is what has been verified end-to-end against the queryjobs API in a deployed app. Use the pattern above, and treat the post as context for the surrounding workflow (time ranges, result handling, export).

## The 207 Multi-Status Gotcha

CrowdStrike APIs may return `207 Multi-Status` responses that look successful but contain embedded errors. Check the errors array:

```python
response = falcon.perform_action(action_name="contain", ids=host_ids)

if response["status_code"] == 207:
    errors = response.get("body", {}).get("errors", [])
    rate_limited = [e for e in errors if e.get("code") == 429]
    if rate_limited:
        return Response(body={"error": "Rate limited", "failed_ids": [e.get("id") for e in rate_limited]}, code=429)
```

## Testing

Mock Falcon APIs in tests instead of making real API calls (they are slow, flaky, and quota-consuming):

```python
def test_get_alerts_success():
    mock_falcon = Mock()
    mock_falcon.query_alerts_v2.return_value = {
        "status_code": 200,
        "body": {"resources": ["alert-001", "alert-002"]}
    }
    mock_falcon.get_alerts_v2.return_value = {
        "status_code": 200,
        "body": {"resources": [{"id": "alert-001", "severity": 80}]}
    }

    with patch('falconpy.Alerts', return_value=mock_falcon):
        from main import get_alerts
        request = Mock(spec=Request)
        request.params = {"limit": "10"}
        response = get_alerts(request, None, Mock())
        assert response.code == 200
        assert len(response.body["alerts"]) == 1
```

## Local Testing

```bash
export FALCON_CLIENT_ID="your-client-id"
export FALCON_CLIENT_SECRET="your-client-secret"
cd functions/my-function && python3 main.py
curl -X GET http://localhost:8081/api/alerts?limit=10
```

## OAuth Scopes for manifest.yml

Every FalconPy service class call requires the correct OAuth scope(s) declared in your manifest's `auth.scopes` array. Without the right scopes, the function gets a 403 at runtime. `foundry apps validate` does NOT catch missing scopes — it only fails at runtime.

> **⚠️ Scope names don't always match class names.** The `Hosts` class requires `devices:read`, not `hosts:read`. Always use this table rather than guessing from class names.

> **Built-in capabilities don't need scopes.** API integrations, collections, workflows, and LogScale ingestion work without declaring their scopes when used through Foundry's built-in SDK patterns (`falcon.apiIntegration()`, `CustomStorage()` for app collections, etc.). Only declare scopes when calling Falcon platform APIs directly via FalconPy service classes.

### Scope Reference (verified from production sample apps)

Each row maps a FalconPy method actually called in a sample function to the scope declared in that app's manifest.

| FalconPy Class | Methods | Required Scope(s) | Verified In |
|---|---|---|---|
| `Hosts` | `get_device_details` | `devices:read` | foundry-sample-functions-python |
| `Intel` | `query_indicator_ids` | `falconx-indicators:read` | foundry-sample-zscaler-internet-access |
| `IdentityProtection` | `graphql`, `query_sensors`, `get_sensor_details` | `identity-graphql:write`, `identity-entities:read` | foundry-sample-idp-notifications |
| `IdentityProtection` | `query_policy_rules`, `get_policy_rules`, `delete_policy_rules` | `identity-policy-rules:read`, `identity-policy-rules:write` | foundry-sample-servicenow-idp |
| `NGSIEM` | `upload_file` | `humio-auth-proxy:write` | foundry-sample-ngsiem-importer |
| `NGSIEM` | `start_search`, `get_search_status` | `humio-auth-proxy:read` | Verified against a live CID (200 + results); see LogScale Queries section |
| `FoundryLogScale` | `ingest_data` | `app-logs:read`, `app-logs:write` | foundry-sample-logscale |
| `FirewallManagement` | `create_rule_group`, `query_events`, `get_events` | `firewall-management:read`, `firewall-management:write` | foundry-sample-category-blocking |
| `HostGroup` | `query_host_groups`, `get_host_groups` | `host-group:read`, `host-group:write` | foundry-sample-category-blocking |

**Go functions (gofalcon) require the same scopes.** The table above uses FalconPy class/method names, but the underlying Falcon API scopes are identical regardless of SDK. If your Go function calls the RTR admin API, declare `real-time-response-admin:write`. If it manages incidents, declare `incidents:read`, `incidents:write`.

### How to declare scopes

```yaml
# manifest.yml
auth:
    scopes:
        - devices:read
        - falconx-indicators:read
    permissions: {}
    roles: []
```

### When unsure about the correct scope

If you're using a FalconPy method not in this table:
1. Check the method's HTTP verb and API path in FalconPy source — GET typically needs `:read`, POST/PUT/PATCH/DELETE typically needs `:write`
2. The scope prefix is usually the **API path prefix** (e.g., `/iocs/...` → `iocs`, `/devices/...` → `devices`), but exceptions exist (`Hosts` → `devices`, `NGSIEM` → `humio-auth-proxy`)
3. When ambiguous, **ask the user** which scopes to include rather than guessing

## Falcon Severity Values

CrowdStrike APIs return severity as **integers** (1-5) or display names. When integrating with external systems (Jira, ServiceNow, email), map them explicitly:

| Falcon Severity | Display Name | Typical External Mapping |
|----------------|--------------|--------------------------|
| 1 | Informational | Low / Lowest |
| 2 | Low | Low |
| 3 | Medium | Medium |
| 4 | High | High |
| 5 | Critical | Highest / Critical |

Use `max_severity_displayname` for FQL filters (string comparison) or `max_severity` for numeric comparison. When passing severity to external ticketing systems, always map to their expected format rather than passing the raw value through.

## Common Pitfalls

- **Writing OAuth code or credential management.** Auth is automatic inside FDK handlers. The zero-arg pattern (`Hosts()`, `Alerts()`) handles all auth. (Go requires `fdk.FalconClientOpts()` -- see above.)
- **Using `requests` library instead of CrowdStrike SDKs.** SDKs handle auth, retries, pagination, and region discovery.
- **Passing credentials explicitly to constructors.** Use zero-arg constructors (`Alerts()`, `Hosts()`). Do NOT write `IOC(client_id=os.environ["FALCON_CLIENT_ID"], client_secret=...)` -- this breaks context-based auth in the Foundry cloud.
- **Writing Falcon API calls outside of FDK handler functions.** The handler pattern is required for automatic auth injection.
- **Not handling 207 Multi-Status.** These responses look successful but may contain embedded errors.

## Use Cases

For real-world implementation patterns, see:
- `use-cases/python-functions.md` — Python handler patterns, SDK usage, testing

## Reference Implementations

- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: Reference Python patterns. See also [Dive into Falcon Foundry Functions with Python](https://www.crowdstrike.com/tech-hub/ng-siem/dive-into-falcon-foundry-functions-with-python/).
- **[foundry-sample-anomali-threatstream](https://github.com/CrowdStrike/foundry-sample-anomali-threatstream)**: Side-by-side Go and Python auth patterns.
- **[foundry-sample-detection-translation](https://github.com/CrowdStrike/foundry-sample-detection-translation)**: CrowdStrike alerts API from functions.
- **[foundry-sample-threat-intel](https://github.com/CrowdStrike/foundry-sample-threat-intel)**: CrowdStrike Intelligence APIs from functions.
- **[foundry-sample-idp-notifications](https://github.com/CrowdStrike/foundry-sample-idp-notifications)**: Falcon IdP domain and connector monitoring.

Referenced files: 1

fusion-redirect2.5 KB

View saved version →

---
name: fusion-redirect
description: TRIGGER when user asks for a "standalone Falcon Fusion workflow" that needs NO Foundry app — just a trigger plus actions that already exist in their CID, with no UI, function, collection, or custom API integration to build. DO NOT TRIGGER when the request needs anything built (a custom action, a UI page, a function, a collection) — that is a Foundry app and development-workflow owns it. This skill exists so the redirect works without hooks and yields to the real Falcon Fusion plugin when both are loaded.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, fusion, redirect]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: routing
---

# Falcon Fusion Redirect

When this skill triggers, the user's request is not a Falcon Foundry task. It is a standalone Fusion workflow and belongs to the sibling Falcon Fusion plugin.

## What to do

Do NOT scaffold a Foundry app. Do NOT produce a `manifest.yml`. Do NOT hand-write workflow YAML with placeholder action IDs.

Respond with all three:

1. A clear statement that this request does not need a Foundry app
2. The plugin name: **`crowdstrike-falcon-fusion`**
3. How to install it: `/plugin install crowdstrike-falcon-fusion` or https://github.com/CrowdStrike/fusion-skills

## Why this exists

The correct tool for a standalone Fusion workflow is the Falcon Fusion plugin. It discovers real action IDs from the live API, validates YAML against the platform schema, and imports and releases to the CID. A redirect skill with no artifact is strictly better than producing a YAML block with placeholder IDs that cannot deploy.

When both plugins are loaded, the Fusion plugin's own `workflows` skill matches the same prompt and handles it directly. This skill fires only when the Fusion plugin is absent, ensuring the user is told about it rather than left with an incomplete workaround.

## Distinguishing a redirect from a Foundry workflow

| Signal in the prompt | Route |
|---|---|
| "standalone workflow", "just a workflow", "no app needed" | Here (redirect) |
| Uses only existing actions: contain host, Slack, email, print data | Here (redirect) |
| Needs a custom API integration, a UI, a function, or a collection built | `development-workflow` (Foundry app) |
| Workflow is part of an app already being built | `workflows-development` (sub-skill) |

The key test: does the user need something *built* that does not exist yet, or do they need existing pieces *wired together*? Building is Foundry. Wiring is Fusion.
security-patterns7.39 KB

View saved version →

---
name: security-patterns
description: Security patterns for Falcon Foundry apps including OAuth scopes, RBAC, input validation, UI security, and credential management. TRIGGER when user asks to "configure OAuth scopes", "secure a Foundry app", "handle secrets", "add input validation", or needs to review a Foundry app for security concerns (XSS, CSP, credential management). Also trigger during pre-deployment security reviews.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, oauth, rbac, xss, csp]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: security
---

# Foundry Security Patterns

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry security architect**.
>
> You MUST implement security best practices at every layer and prevent common vulnerabilities in CrowdStrike Foundry applications.

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

Security patterns for Falcon Foundry app development covering authentication, input validation, UI security, and platform-specific considerations. Foundry apps run on a cybersecurity platform — security is a core requirement.

## RBAC (Role-Based Access Control)

| Capability | RBAC Supported |
|-----------|----------------|
| Collections | Yes |
| Dashboards | Yes |
| Functions | Yes |
| UI extensions / pages / navigation | Yes |
| RTR scripts | Yes |
| API integrations | **No** |
| Queries | **No** |
| Workflows | **No** |

## API Scope Management

Scopes control which Falcon Platform APIs the app can access. Format: `<source>:<operation>` (e.g., `devices:read`, `detects:write`).

| Scopes set automatically | Scopes need explicit addition |
|--------------------------|-------------------------------|
| API integrations, Collections, Dashboards, Queries, UI navigation, UI sockets, Workflows | Functions, UI extensions, UI pages, RTR scripts |

```bash
foundry auth roles create --name "Analyst" --description "Read-only analyst access"
foundry auth scopes add --scope "devices:read" --scope "detects:read"
```

Only use `foundry auth scopes add` for Falcon Platform API scopes needed by functions, UI extensions, UI pages, or RTR scripts. OAuth scopes for CLI-created artifacts are managed automatically.

### Minimal Scope Principle

Request only the scopes your app needs. Broad scopes like `alerts:*` or `hosts:*` increase the blast radius if the app is compromised.

```yaml
oauth_scopes:
  - "alerts:read"        # Read alerts — avoid "alerts:write" unless needed
  - "detections:read"    # Read detections
  - "hosts:read"         # Device information
```

## Credential Security

Credentials MUST be in environment variables, not in code. FalconPy handles credential discovery automatically inside FDK handlers (see functions-falcon-api):

```python
# Inside FDK handler — auth is automatic
falcon = Alerts()  # Do not pass credentials

# Outside handler (local testing) — use env vars
# FALCON_CLIENT_ID and FALCON_CLIENT_SECRET read automatically
```

## Input Validation

### JSON Schema for Collections

Use strict schemas to prevent data corruption and injection:

```json
{
  "type": "object",
  "required": ["timestamp", "event_type", "source"],
  "additionalProperties": false,
  "properties": {
    "event_type": {
      "type": "string",
      "enum": ["alert", "detection", "incident"]
    },
    "source": {
      "type": "string",
      "pattern": "^[a-zA-Z0-9_-]+$",
      "maxLength": 50
    }
  }
}
```

### API Response Sanitization

Sanitize CrowdStrike API responses before storing in Collections: remove sensitive fields (`raw_log`, `internal_id`, `system_metadata`), strip script injection, escape HTML entities, and truncate strings. See [references/security-examples.md](references/security-examples.md) for full implementation.

### Function Input Validation

Validate that input is a dict and enforce size limits (e.g., 10KB) to prevent abuse. Return generic error messages — MUST NOT expose stack traces or internal state in responses.

## UI Security

### XSS Prevention

- **React:** Use `DOMPurify.sanitize()` before any `dangerouslySetInnerHTML`. React auto-escapes `{}` expressions.
- **Vue:** Use `DOMPurify.sanitize()` in a computed property before `v-html`. Vue auto-escapes `{{ }}` expressions.

For complete React and Vue XSS prevention components, see [references/security-examples.md](references/security-examples.md).

### Content Security Policy

Configure CSP in `manifest.yml` for UI pages:

```yaml
ui:
  pages:
    - name: my-page
      csp:
        connect_src:
          - "'self'"
          - "https://api.crowdstrike.com"
        img_src:
          - "'self'"
          - "data:"
        script_src:
          - "'self'"
```

### Iframe Security for Extensions

Extensions run in sandboxed iframes. Validate message origins against Falcon console domains:

```typescript
const allowedOrigins = [
  'https://falcon.crowdstrike.com',
  'https://falcon.eu-1.crowdstrike.com',
  'https://falcon.us-gov-1.crowdstrike.com',
];

window.addEventListener('message', (event) => {
  if (!allowedOrigins.includes(event.origin)) return;
  // Process event.data
});
```

For the full `SecureConsoleMessaging` class, see [references/security-examples.md](references/security-examples.md).

## Manifest Security Configuration

```yaml
app:
  name: "my-security-app"

oauth_scopes:
  - "alerts:read"
  - "hosts:read"

functions:
  - name: "process-alerts"
    language: "python"
    max_exec_duration_seconds: 30  # Prevent runaway execution
    max_exec_memory_mb: 128        # Limit resource usage

collections:
  - name: "audit_logs"
    ttl: 86400  # Auto-expire sensitive data (24 hours)
```

## Test Data Security

- Use only RFC 1918 IPs (`192.168.x.x`, `10.x.x.x`) in mock data
- Use obviously fake hostnames and users (`test-workstation-01`, `test_user`)
- Validate mock data does not contain production indicators (`crowdstrike.com`, `falcon-`, `prod-`)
- Test XSS prevention with known attack vectors (`<script>`, `javascript:`, `onerror=`)

See [references/security-examples.md](references/security-examples.md) for mock data validation and CI/CD security patterns.

## Pre-Deployment Checklist

- [ ] OAuth scopes: minimal required permissions only
- [ ] Input validation: JSON schemas enforce strict validation
- [ ] XSS prevention: all user data sanitized before rendering
- [ ] CSP headers: Content Security Policy configured
- [ ] Postmessage security: origin validation implemented
- [ ] Secret management: no hardcoded credentials
- [ ] Function security: input size limits and timeout controls
- [ ] Collection security: access controls and data sanitization
- [ ] Test data: only fake data in tests and development
- [ ] Error handling: no sensitive data in error messages or logs

## Reading Guide

| Task | Reference |
|------|-----------|
| Sanitization, command injection prevention, secure templates | [references/security-examples.md](references/security-examples.md) |
| CI/CD security pipeline (GitHub Actions) | [references/security-examples.md](references/security-examples.md) |
| PostMessage class, mock data validation | [references/security-examples.md](references/security-examples.md) |
| Token lifecycle, antipatterns, manifest security, performance | [references/security-examples.md](references/security-examples.md) |

Referenced files: 1

ui-development19.3 KB

View saved version →

---
name: ui-development
description: Build UI pages and extensions for Falcon Foundry apps using React or Vue with the Shoelace design system and Foundry-JS. TRIGGER when user asks to "create a UI page", "build a UI extension", "add a Shoelace component", "call an API from the UI", runs `foundry ui pages create` or `foundry ui run`, or needs help with Vite config, Foundry-JS, or Falcon console theming. DO NOT TRIGGER for backend functions, workflow YAML, or collection schemas.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, ui, react, vue, shoelace]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: frontend
---

# Foundry UI Development

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry UI specialist**.
>
> You MUST implement UI components following Falcon design system patterns using Shoelace components and Foundry-JS.
>
> **IMMEDIATE ACTIONS REQUIRED:**
> 1. Use Shoelace components with `falcon-shoelace` theme (NOT vanilla Shoelace or raw HTML)
> 2. Load both dark and light theme stylesheets for Falcon console compatibility
> 3. Coordinate with `foundry ui run` for live development
> 4. Apply iframe security patterns for all extensions

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

Falcon Foundry UI pages and extensions use React or Vue with the Shoelace design system (Falcon-themed) and Foundry-JS for platform integration.

## Pages vs Extensions

If the user doesn't specify page or extension, **ask which they prefer** before scaffolding. Present this table to help them decide:

| | UI Pages | UI Extensions |
|---|---|---|
| **What** | Standalone applications | Console-embedded components |
| **Where** | Full-page view in Falcon console | Sidebar widget in detection/host/incident pages |
| **Sockets** | N/A | One per extension (see socket table below) |
| **Use when** | Complex interactions, multiple views, dashboards | Contextual enrichment, quick-glance data |
| **Framework** | Vue, React, or Vanilla JS | Vue, React, or Vanilla JS |

## CLI Scaffolding

```bash
# Create a React page
foundry ui pages create --name "my-page" --description "Page description" --from-template React --homepage --no-prompt

# Create a Vanilla JS page (no npm install or build step needed)
foundry ui pages create --name "my-page" --description "Page description" --from-template "Vanilla JS" --homepage --no-prompt

# Add navigation entry (separate step — --no-prompt skips this during page creation)
foundry ui navigation add --name "My Page" --path / --ref pages.my-page

# Create an extension targeting a console socket
# REQUIRED: --sockets must be specified — omitting it launches an interactive picker that hangs with Error: EOF
# REQUIRED: Use ONLY values from the Extension Socket Locations table below — do NOT guess socket IDs
foundry ui extensions create --name "my-ext" --description "Description" --from-template React --sockets "activity.detections.details" --no-prompt

# Create a Vanilla JS extension (no npm install or build step needed)
foundry ui extensions create --name "my-ext" --description "Description" --from-template "Vanilla JS" --sockets "activity.detections.details" --no-prompt
```

**Vanilla JS** needs no `npm install` or `npm run build`. The importmap loads foundry-js from CDN. Use it for simple extensions that display data or make API calls without complex state management. Deploy works with just the raw `src/` files.

The blueprint output is deterministic — see [references/blueprint-templates.md](references/blueprint-templates.md) for exact file contents, Shoelace import patterns, and API integration calling examples.

> **🚫 DO NOT MODIFY `path` or `entrypoint` in manifest.yml**
>
> The CLI sets `path` and `entrypoint` correctly during scaffolding. **Never edit these values.** The correct CLI-generated format uses full paths from the app root:
> ```yaml
> # Page — this is CORRECT, do not shorten
> path: ui/pages/my-page/src/dist
> entrypoint: ui/pages/my-page/src/dist/index.html
>
> # Extension — this is CORRECT, do not shorten
> path: ui/extensions/my-ext/src/dist
> entrypoint: ui/extensions/my-ext/src/dist/index.html
> ```
> These long paths are NOT doubled — they are the correct values the CLI generates. Shortening `entrypoint` to `src/dist/index.html` breaks the app. If a deploy error mentions entrypoint or file path, you likely changed `vite.config.js` — revert your changes. The scaffolded config is correct.

## Vite Build Configuration

> **🚫 DO NOT MODIFY `vite.config.js`**
>
> The React blueprint's `vite.config.js` is **turnkey** — it works correctly as scaffolded. Do not change ANY values in it. Specifically:
> - **Do not change `base: './'`** — not to `''`, not to `'/'`, not to anything else. `'./'` is correct.
> - **Do not change `root: 'src'`** — the manifest expects builds at `src/dist/`.
> - **Do not remove `noAttr()`** — required for Foundry's sandboxed iframe.
>
> The blueprint, manifest, and CLI are coordinated. Changing any config value breaks this coordination and causes deploy failures. Just edit your React/JS component code and deploy.

## Shoelace Design System

Install the Falcon-themed Shoelace package:

```bash
npm install @crowdstrike/falcon-shoelace
```

Import the Falcon-themed stylesheet. The React blueprint's `index.html` already includes this as a `<link>` tag, so **do not add JS imports for it**:

```css
/* Already in index.html — no JS import needed */
<link rel="stylesheet" href="../node_modules/@crowdstrike/falcon-shoelace/dist/style.css" />
```

If importing from JS (e.g., Vanilla JS apps without the blueprint's index.html):

```typescript
import '@crowdstrike/falcon-shoelace/dist/style.css';
```

Set the Shoelace asset base path:

```typescript
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path';
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.x/dist/');
```

Use `var(--sl-*)` design tokens for all styling instead of hardcoding colors. This ensures the UI adapts correctly when users switch between light and dark mode in the Falcon console:

```css
.my-component {
  color: var(--sl-color-neutral-900);
  background: var(--sl-color-neutral-0);
  padding: var(--sl-spacing-medium);
  border-radius: var(--sl-border-radius-medium);
}
```

For extended Shoelace component catalog and CSS customization, see [references/shoelace-reference.md](references/shoelace-reference.md).

For theming, dark/light mode switching, and design token values, see [references/falcon-theming.md](references/falcon-theming.md).

## Foundry-JS

```javascript
import FalconApi from '@crowdstrike/foundry-js';

const falcon = new FalconApi();
await falcon.connect();

// Apply Falcon console theme
const theme = await falcon.theme();
document.documentElement.classList.add(`sl-theme-${theme}`);
```

> **⚠️ `connect()` is async.** In React, `falcon.connect()` must be called inside a `useEffect` and navigation must only be accessed AFTER connect resolves. Use React state (`isInitialized`) as the `useMemo` dependency — not `falcon.isConnected` (which is a plain object property, not reactive state):
>
> ```jsx
> const [isInitialized, setIsInitialized] = useState(false);
> const falcon = useMemo(() => new FalconApi(), []);
> const navigation = useMemo(() => {
>   return isInitialized ? falcon.navigation : undefined;
> }, [isInitialized]);
> ```
>
> Gate child rendering on `isInitialized` state. See [references/react-patterns.md](references/react-patterns.md) for the full `FalconApiProvider` pattern.

### Calling API Integrations

Use `falcon.apiIntegration()` for third-party APIs. Use `falcon.api.get()` for CrowdStrike Falcon APIs.

```javascript
const apiIntegration = falcon.apiIntegration({
  definitionId: 'Okta',        // Must match name in manifest.yml api_integrations
  operationId: 'listUsers'     // Must match operationId in OpenAPI spec
});
const result = await apiIntegration.execute({ request: { params: {} } });

// Response is wrapped: access via resources[0]
const body = result.resources?.[0]?.response_body;
const status = result.resources?.[0]?.status_code;
```

Query and path parameters must match the types declared in the OpenAPI spec. `execute()` types params as `Record<string, unknown>`, so a quoted number compiles and ships fine, then fails server-side schema validation at runtime with `got string want integer`. The extension still renders, so this looks like an API or credential error rather than a bug in your code.

```javascript
// spec declares: - name: limit / schema: { type: integer }
request: { params: { query: { limit: 25 } } }    // ✅ number
request: { params: { query: { limit: '25' } } }  // ❌ got string want integer
```

Check the `schema.type` of each parameter in the spec before passing it. Numbers and booleans are unquoted; only `type: string` parameters take quotes.

### Collection Operations

```javascript
const collection = falcon.collection({ collection: 'my_collection' });

// CRUD operations
await collection.write('item-key', { name: 'Item 1', status: 'active' });
const item = await collection.read('item-key');
await collection.delete('item-key');

// List all items (returns IDs, then read each)
const result = await collection.list({ start: 0, limit: 100 });
const itemIds = result.resources || [];
for (const id of itemIds) {
  const item = await collection.read(id);
  // Add 50ms delay between reads to avoid rate limiting
}

// Search with FQL filter
const results = await collection.search({ filter: "status:'active'" });
```

### Workflow Execution

```javascript
// Execute an on-demand workflow by name
const triggerResult = await falcon.api.workflows.postEntitiesExecuteV1(
  { user_name: 'Developer' },           // workflow input parameters
  { name: 'My Workflow', depth: 0 }     // config: workflow name + depth
);

// Poll for results using the execution ID
const execId = triggerResult.resources?.[0];
const result = await falcon.api.workflows.getEntitiesExecutionResultsV1({
  ids: [execId]
});
const execution = result.resources?.[0];
// Poll until execution.status is 'Completed', 'Failed', or 'Cancelled'
```

### Cloud Functions

```javascript
const cloudFunction = falcon.cloudFunction({
  name: 'my_function',
  version: 1
});

// Fluent API — chain .path() with HTTP method
const result = await cloudFunction.path('/greet').post({ name: 'User' });
const data = await cloudFunction.path('/items?status=active').get();
await cloudFunction.path('/items/123').delete();
```

### LogScale

```javascript
// Write events
await falcon.logscale.write({ event_type: 'user_login', username: 'jdoe' });

// Dynamic query
const results = await falcon.logscale.query({
  name: 'LogScaleRepo',
  search_query: 'event_type=user_login',
  start: '24h',
  end: 'now'
});

// Saved query
const saved = await falcon.logscale.savedQuery({
  id: 'saved-query-id',
  start: '7d',
  mode: 'sync',
  parameters: {}
});
```

### Events

```javascript
// Listen to Falcon console events (data, connect, disconnect, error, navigation)
falcon.events.on('data', (data) => console.log('Data event:', data));
falcon.events.on('navigation', (data) => console.log('Nav event:', data));

// Clean up listeners on unmount
falcon.events.off('data', handler);
```

For full React component examples, see [references/react-patterns.md](references/react-patterns.md).
For full Vue component examples, see [references/vue-patterns.md](references/vue-patterns.md).

## Development Servers

- **`foundry ui run`**: Serves only the UI in Falcon console dev mode (port 25678). Use during UI-focused development.
- **`foundry apps run`**: Starts the full app locally (validates manifest on startup). Use when testing UI + functions + integrations together.

**CRITICAL: Run from the app root directory.** Both `foundry ui run` and `foundry apps run` resolve manifest paths relative to `os.Getwd()`. After `cd`-ing into a UI page/extension directory for `npm install && npm run build`, always `cd` back to the app root before running any `foundry` app commands. Running from a subdirectory causes "file not found" or doubled-path errors.

If the UI calls API integrations, collections, or functions, deploy those backend capabilities first via `foundry apps deploy`. `foundry ui run` only serves UI assets locally — backend capabilities resolve from the cloud.

### Development Mode vs Preview Mode

Toggle via the **Developer tools** (`</>`) icon in the Falcon console toolbar:

| Feature | Development Mode | Preview Mode |
|---------|-----------------|--------------|
| Activation | `foundry ui run` + enable in Developer tools | Deploy, then enable in Developer tools |
| Source | Local build from your machine (polls localhost:25678) | Deployed build in the cloud |
| Purpose | Live UI iteration with hot reload | Test deployed UI before release |

Only one mode at a time. Disable one before enabling the other.

## Iframe Communication

Extensions must validate message origins:

```typescript
const allowedOrigins = [
  'https://falcon.crowdstrike.com',
  'https://falcon.eu-1.crowdstrike.com',
  'https://falcon.us-gov-1.crowdstrike.com',
];

window.addEventListener('message', (event: MessageEvent) => {
  if (!allowedOrigins.includes(event.origin)) return;
  // Process event.data
});
```

## Extension Socket Locations

Run `foundry ui extensions list-sockets` to get the current list of available sockets. Use the **technical ID** (not human-readable name) with `--sockets`. The extension renders in the detail panel reached by the navigation below — open an individual record (detection, case, host, lead, or execution) to see it.

| Display Name | Technical ID for `--sockets` | Console Navigation |
|-------------|------------------------------|--------------------|
| Endpoint detection details | `activity.detections.details` | Endpoint security › Monitor › Endpoint detections |
| Identity Protection detection details | `identity.detections.details` | Identity protection › Detections |
| Next-Gen SIEM cases details | `xdr.cases.panel` | Next-Gen SIEM › Cases |
| Next-Gen SIEM workbench details | `ngsiem.workbench.details` | Next-Gen SIEM › Cases › open a case › workbench graph canvas › click a node |
| Host management host details | `hosts.host.panel` | Host setup and management › Host management |
| Automated leads details | `automated-leads.leads.details` | Next-Gen SIEM › Automated leads |
| Workflow execution details | `workflows.executions.execution.details` | Fusion SOAR › Workflows › open an execution |

## Common Pitfalls

> **Note:** The first three pitfalls below about `vite.config.js`, `npm install`, and `npm run build` apply to the React template only. Vanilla JS extensions have no build step — deploy the raw `src/` files directly.

- **Running `foundry` commands from a UI subdirectory.** After `cd ui/extensions/my-ext && npm install && npm run build`, you MUST `cd` back to the app root before running `foundry apps validate`, `foundry apps deploy`, or `foundry ui run`. The CLI resolves manifest paths relative to cwd — running from a subdirectory produces doubled paths like `ui/extensions/my-ext/ui/extensions/my-ext/src/dist/index.html`.
- **NEVER edit manifest.yml `path` or `entrypoint` values.** The CLI sets these correctly. The format `ui/extensions/my-ext/src/dist/index.html` is NOT a doubled path — it is correct. If you see a deploy path error, you likely changed `vite.config.js` — revert your changes.
- **NEVER modify `vite.config.js`.** The blueprint is turnkey. Do not change `base: './'` to `''` or anything else. Do not change `root: 'src'`. Do not remove `noAttr()`. Just edit your React/JS code and deploy.
- **Omitting `--sockets` on extension create.** This launches an interactive picker that hangs with `Error: EOF`. Always provide `--sockets "socket.id"` on the command line. Run `foundry ui extensions list-sockets` to see available sockets — do not guess or fabricate socket names.
- **Importing vanilla Shoelace themes.** Use `@crowdstrike/falcon-shoelace` for Falcon console styling.
- **Loading only light theme.** The Falcon console supports dark mode — users see broken styling without both themes.
- **Hardcoding colors.** Use `var(--sl-*)` design tokens so the UI adapts to theme changes.
- **Expecting backend to work with `foundry ui run`.** The dev server only serves UI — deploy backend capabilities first.
- **Shoelace dialogs/drawers white in dark mode.** Override `--sl-panel-background-color` and `--sl-color-neutral-0` with `var(--ground-floor)`. See [references/shoelace-reference.md](references/shoelace-reference.md).
- **Using Tailwind arbitrary values with prebuilt toucan CSS.** Values like `max-h-[400px]` require JIT compilation. Use inline styles instead when using the prebuilt `tailwind-toucan-base/index.css`.
- **Quoting numeric query parameters.** `execute()` accepts `Record<string, unknown>`, so `limit: '25'` passes type-checking and fails server-side with `got string want integer`. Match the `schema.type` declared in the OpenAPI spec — the extension still renders, so this reads as an API error rather than a code bug.
- **Missing CSP for Shoelace icons.** The Foundry CSP only allows `assets.foundry.crowdstrike.com`. If using `setBasePath()` with `cdn.jsdelivr.net`, you must add it to `connect-src` and `img-src` in the manifest's `content_security_policy`. Alternatively, copy icon assets to your `dist/` folder and set a relative base path to avoid CDN dependencies entirely.

## Reading Guide

| Task | Reference |
|------|-----------|
| Blueprint file contents, editing strategy | [references/blueprint-templates.md](references/blueprint-templates.md) |
| Shoelace component catalog, CSS customization | [references/shoelace-reference.md](references/shoelace-reference.md) |
| Dark/light theming, design tokens | [references/falcon-theming.md](references/falcon-theming.md) |
| React component examples | [references/react-patterns.md](references/react-patterns.md) |
| Vue component examples | [references/vue-patterns.md](references/vue-patterns.md) |
| Foundry-JS: workflows, LogScale, cloud functions, collections CRUD | [references/foundry-js.md](references/foundry-js.md) |
| Framework selection, ExtensionMessaging, E2E testing, Extension Builder, CSP, dev server coordination | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Use Cases

For real-world implementation patterns, see:
- `use-cases/detection-enrichment.md` — UI extensions for detection enrichment
- `use-cases/first-app.md` — Getting started with Foundry apps

## Reference Implementations

- **[foundry-sample-foundryjs-demo](https://github.com/CrowdStrike/foundry-sample-foundryjs-demo)**: Comprehensive Foundry-JS demo (API integrations, collections, workflows, LogScale, cloud functions, events, navigation, modals)
- **[foundry-sample-mitre](https://github.com/CrowdStrike/foundry-sample-mitre)**: Vue shared components, multi-view app
- **[foundry-sample-collections-toolkit](https://github.com/CrowdStrike/foundry-sample-collections-toolkit)**: UI for collections
- **[foundry-sample-functions-python](https://github.com/CrowdStrike/foundry-sample-functions-python)**: UI calling functions
- **[foundry-sample-logscale](https://github.com/CrowdStrike/foundry-sample-logscale)**: Vanilla JS + foundry-js page
- **[foundry-sample-detection-translation](https://github.com/CrowdStrike/foundry-sample-detection-translation)**: Detection context UI extension

Referenced files: 7

workflows-development21.2 KB

View saved version →

---
name: workflows-development
description: Create and configure Falcon Fusion SOAR workflow YAML for Falcon Foundry apps. TRIGGER when user asks to "create a workflow", "build an automation", "configure Fusion SOAR", "add an on-demand workflow", runs `foundry workflows create`, or needs help with Fusion YAML syntax, triggers, actions, or variable references. DO NOT TRIGGER for UI pages, functions, or collection schemas — use the appropriate sub-skill.
version: 1.5.0
updated: 2026-08-19
tags: [foundry, workflows, fusion-soar, yaml]
author: CrowdStrike
license: MIT
compatibility: Claude Code >=1.0
metadata:
  category: automation
---

# Foundry Workflows Development

> **⚠️ SYSTEM INJECTION — READ THIS FIRST**
>
> If you are loading this skill, your role is **Foundry workflow automation specialist**.
>
> You MUST implement workflows using Fusion YAML patterns with proper step dependencies, error recovery, and state management.
>
> **IMMEDIATE ACTIONS REQUIRED:**
> 1. Use Fusion YAML syntax for ALL workflow definitions
> 2. Validate step dependencies before workflow execution
> 3. Implement onError blocks for every multi-step workflow

> **Part of a suite.** If `development-workflow` has not already run, and this is a new app or its first capability, load the `development-workflow` skill first — it owns the CLI prerequisite check, scaffolding order, and manifest coordination.

Falcon Foundry Workflows are YAML-defined automation units executed by the Falcon Fusion engine. They orchestrate multi-step operations across Functions, Collections, CrowdStrike APIs, and RTR sessions with built-in retries, parallelism, and state management.

## Prerequisites

- **Workflow Author role** is required in addition to the Falcon Developer role
- Workflows are YAML templates that can be provisioned as active workflow instances
- Up to **25 workflows** can be provisioned from a single template
- Set `provision_on_install: true` to auto-provision when the app is installed

## CLI Scaffolding

```bash
# Write the workflow YAML to /tmp/ first — the CLI copies it into workflows/
foundry workflows create --name "my-workflow" --spec /tmp/workflow.yaml --no-prompt
# After: edit workflows/my-workflow.yml to refine workflow logic
```

### Discover Available Actions and Triggers

```bash
foundry workflows actions view --name "send email" --no-prompt              # Look up by name
foundry workflows actions view --name "send email" --no-prompt --output-schema  # Output schema
foundry workflows actions view --name "send email" --no-prompt --mock       # Mock output
foundry workflows triggers view --no-prompt                                 # List triggers
```

> **⚠️ Always use `--no-prompt` on `actions view` and `triggers view`.** The `--name` filter is fuzzy — partial matches trigger an interactive prompt that fails in headless environments (`Error: no TTY available`). If the CLI errors or hangs, use the bundled `action_search.py` — see [references/action-discovery.md](references/action-discovery.md).

## Workflow Structure

### Trigger + Actions Format (standard)

This is the format produced by `foundry workflows create` and used by all production Foundry sample apps:

```yaml
name: list-okta-users
description: On-demand workflow to list Okta users and print results
provision_on_install: true
trigger:
    next:
        - list_users
    name: On demand
    type: On demand
actions:
    list_users:
        id: api_integrations.Okta.listUsers
        next:
            - print_results
        properties: {}
        version_constraint: ~0
    print_results:
        id: aadbf530e35fc452a032f5f8acaaac2a
        properties:
            text_data: "${data['list_users.API_Integration.Custom_Okta.listUsers.body']}"
        version_constraint: ~1
output_fields: []
```

**Trigger types:**

| Type | Format |
|------|--------|
| On demand | `name: On demand`, `type: On demand` |
| Scheduled | `event: Schedule`, `schedule: {time_cycle: "0 */6 * * *", tz: Etc/UTC}` |

> **⚠️ Null-guard trigger parameters:** When run from the Falcon console, the UI prompts users to fill in parameters. However, when triggered via API or from another workflow, parameters may be empty. Guard defensively:
>
> ```yaml
> conditions:
>     check_param:
>         cel_expression: "data['param_name'] != null && data['param_name'] != ''"
>         next:
>             - use_param
>         display:
>             - param_name was provided
>         else:
>             - handle_missing
> ```
>
> Or inline in CEL: `${data[?'param'].orValue("default")}` (preferred) or `${data['param'] != null ? data['param'] : "default"}` (traditional)

**Variable syntax in actions:** Use `${data['action_key.path.to.field']}` CEL expressions. See [Variable References](#variable-references) for the full syntax. Do NOT use `$action_name.output.body` — it passes as a literal string and is not resolved.

**Version constraints:** Every action requires `version_constraint`. The `~N` value pins against the activity's declared `semantic_version` field, not its internal iteration count. The rule:

- `~0` = activity has **no** `semantic_version` defined (functions, API integrations, and some platform actions like "contain device")
- `~1` = activity **has** a `semantic_version` (most platform actions: Print data, Send email, Create/Update variable, Get device details, etc.)

Use `foundry workflows actions view --name "<action>" --no-prompt` to check. If the activity output shows a semantic_version field, use `~1`. If it does not, use `~0`.

```yaml
actions:
    my_function:
        id: functions.my-func.process
        properties: {}
        version_constraint: ~0       # no semantic_version defined
    contain_host:
        id: <contain-device-action-id>
        properties:
            device_id: "${data['trigger.device_id']}"
        version_constraint: ~0       # no semantic_version defined
    print_results:
        id: aadbf530e35fc452a032f5f8acaaac2a
        properties:
            text_data: "${data['my_function.output']}"
        version_constraint: ~1       # has semantic_version
```

### Manifest Configuration

```yaml
# manifest.yml
workflows:
  - name: my-workflow
    path: workflows/my-workflow/workflow.yaml
    permissions: []
```

Trigger type and schedule are defined inside the workflow YAML file (via `trigger:` block), not in the manifest. The manifest only declares the workflow name, path, and optional permissions.

For full RTR multi-host orchestration and investigation pipeline examples, see [references/workflow-examples.md](references/workflow-examples.md).

> RTR scripts are not supported in certified Foundry apps (apps published to the CrowdStrike Store). RTR workflows work in custom/internal apps only.

## Calling Functions from Workflows

Functions referenced in workflow actions (via `id: functions.{name}.{handler}`) must have `workflow_integration` in the manifest. This binds only at creation time, via `foundry functions create --wf-expose` (plus `--input-schema`/`--output-schema`) — hand-editing `manifest.yml` does not work.

If a function was created without it, recreate the function. That changes its `workflow_integration.id`, so update the `id: functions.{name}.{handler}` reference in your workflow YAML in place. Do NOT delete and recreate the workflow itself — see the warning below.

**Deploy error if missing:**
```
❌ Error: referenced function '{name}' and handler '{handler}' does not have workflow_integration properties defined
```

> **⚠️ Reading Falcon alerts, detections, or incidents? It depends on whether the workflow already holds the object.** *Enriching* a detection the workflow was triggered on (query by its ID, e.g. `Ngsiem.detection.id = ?detectID`) → Event Query action, go schemaless. *Fetching a population you don't have* ("summarize all high-severity alerts") → source-of-truth API: a native platform action (e.g. Cases → Search Cases) first, or a FalconPy `Alerts`/`Detects` function when none fits — an Event Query can silently return nothing since NG-SIEM contents are connector-dependent. See [references/event-query-vs-api.md](references/event-query-vs-api.md).

## Calling API Integration Operations

> **💡 Consider HTTP Actions first.** For a simple REST call that doesn't need a custom UI or reusable function, an [HTTP Action](https://www.crowdstrike.com/tech-hub/ng-siem/build-api-integrations-with-falcon-fusion-soar-http-actions/) is faster than an API integration — no app, no OpenAPI spec, no deploy, and 130+ pre-built templates. Use a full API integration when the operation is reused across workflows or paired with functions/UI. See [references/http-actions.md](references/http-actions.md).

Workflows invoke API integration operations using the `api_integrations.{name}.{operationId}` pattern:

> **⚠️ Always use registered integrations.** If an API integration is declared in `manifest.yml`, the workflow MUST call it via `api_integrations.{name}.{operationId}`. Do NOT use a function that makes raw HTTP calls to the same API with hardcoded credentials or template variables like `{{API_TOKEN}}`. The platform manages authentication, rate limiting, and audit logging through the integration.

```yaml
actions:
    list_users_action:
        id: api_integrations.Okta.listUsers    # {name}.{operationId}
        properties: {}
        version_constraint: ~0
        next:
            - print_data

    print_data:
        id: aadbf530e35fc452a032f5f8acaaac2a
        properties:
            text_data: "${data['list_users_action.API_Integration.Custom_Okta.listUsers.body']}"
        version_constraint: ~1
```

The `{name}` must exactly match the `name` field from the `api_integrations` entry in `manifest.yml`, prefixed with `Custom_`. The platform adds `Custom_` to all API integration names in the variable path. The OpenAPI spec must have a matching `operationId` with a properly structured `x-cs-operation-config`:

```yaml
x-cs-operation-config:
  workflow:
    name: listUsers
    description: List all users
    expose_to_workflow: true
    system: false
```

The `workflow` nesting is required — a flat `expose_to_workflow: true` under `x-cs-operation-config` will not work. Auth scopes for CLI-created artifacts are managed automatically.

## Platform Actions

Platform actions (send email, log output, create detection) require platform-specific action IDs. These IDs are verified identical across us-1, us-2, and eu-1 clouds:

| Action Name | ID |
|------------|-----|
| Create variable | `702d15788dbbffdf0b68d8e2f3599aa4` |
| Update variable | `6c6eab39063fa3b72d98c82af60deb8a` |
| Print data | `aadbf530e35fc452a032f5f8acaaac2a` |
| Sleep | `4f1af1ae4c13dc1e3bcd725f8dc0f63b` |
| Send email | `07413ef9ba7c47bf5a242799f59902cc` |
| Request human input - Send email | `d6731c10b24834e2e0f4bd9d390a29c8` |
| Get device details | `6265dc947cc2252f74a5f25261ac36a9` |
| Contain device | `bec9fbeb4999d207937854fd56088107` |

For actions not in this table, use `foundry workflows actions view --name "..." --no-prompt` or the API query in [references/action-discovery.md](references/action-discovery.md). There are 9,000+ platform actions available. MUST NOT guess action IDs — use discovery commands.

### Common Action Properties

**Print data** (`aadbf530e35fc452a032f5f8acaaac2a`):

Print data has three input properties: `fields` (array — dropdown of trigger/workflow metadata), `text_data` (string — general-purpose), and `custom_json` (object only). Use `text_data` for API integration responses since `body` may be an array.

```yaml
    print_data:
        id: aadbf530e35fc452a032f5f8acaaac2a
        properties:
            text_data: "${data['list_users_action.API_Integration.Custom_Okta.listUsers.body']}"
        version_constraint: ~1
```

The data path follows the pattern: `action_key.API_Integration.Custom_{IntegrationName}.{operationId}.{field}`. The platform adds `Custom_` to all API integration names in the variable path. Use the **Workflow data** panel in the workflow editor to copy the exact path for any field — click the data pill and it copies the correct `${data['...']}` expression to your clipboard.

**Send email** (`07413ef9ba7c47bf5a242799f59902cc`):

Use Print data as the primary output action. Only add Send email when the user explicitly requests it. In headless/automated runs (`claude -p`), use Print data only — skip Send email entirely since the recipient cannot be prompted.

In interactive mode, when the user requests email, **ask for their email address with the assistant's available input mechanism**. If none exists, ask in chat. Never guess or infer the email from context. The `to` field must contain a real email address — placeholders like `user@example.com` fail at runtime.

```yaml
    send_email:
        id: 07413ef9ba7c47bf5a242799f59902cc
        properties:
            to:
                - recipient@company.com    # Ask user for real address, or mark as parameterized
            subject: "Email subject"
            msg: "${data['list_users_action.API_Integration.Custom_Okta.listUsers.body']}"
            msg_type: "text"
        version_constraint: ~1
```

## Variable References

Falcon Fusion SOAR uses [Common Expression Language (CEL)](https://github.com/google/cel-spec/blob/master/doc/langdef.md) for data references. All variable references use `${data['...']}` expression syntax:

| Syntax | Description |
|--------|-------------|
| `${data['action_key.API_Integration.Custom_Name.operationId.body']}` | Response body from an API integration action |
| `${data['action_key.API_Integration.Custom_Name.operationId.body']}[0].field` | Access a field in the first element of an array response |
| `${data['action_key.output.field']}` | Field from a platform action's output |
| `${data['param_name']}` | On-demand trigger parameter value (use the parameter name directly, no prefix) |

**CRITICAL:** Do NOT use `$action_name.output.body` — this passes as a literal string and is NOT resolved at runtime. Always use `${data['...']}` expressions.

The `action_key` is the YAML key of the action (e.g., `list_users` from `actions: list_users:`), NOT the action's `id`. The integration name in the variable path is `Custom_{name}` where `{name}` is the `name` field in your `api_integrations` manifest entry (spaces become underscores). Use the **Workflow data** panel in the workflow editor to copy exact data paths — it produces the correct expression when you click a data pill.

### CEL Expressions

Falcon Fusion SOAR supports CEL for data transformations, conditions, and field access. Common patterns:

```yaml
# Null-safe field access — optional pattern (preferred)
"${data[?'action.field'].orValue(\"default\")}"

# Traditional null check
"${data['action.field'] != null ? data['action.field'] : \"default\"}"

# Array element access (index goes INSIDE the ${...}, not after it)
"${data['action.API_Integration.Custom_Name.op.body'][0]}"
```

**`has()` only works on retrieved objects, not data store keys.** `has(data['key'])` fails with `Q0910`; use `data['key'] != null` for keys, or `has(data['var'].field)` to check a field on an already-retrieved object.

CrowdStrike adds [custom CEL extensions](https://docs.crowdstrike.com/r/k223d842) (`cs.json.decode()`, `cs.ip.valid()`, `cs.timestamp.parse()`, etc.). For the full pattern catalog, the `has()`/`!= null`/optional decision guide, and extension details, see [references/cel-expressions.md](references/cel-expressions.md).

## Control Flow

**Loops** iterate over arrays or paginate with cursor-based conditions. Loops are self-contained sub-workflows at the root `loops:` level:

```yaml
loops:
    DeviceLoop:
        for:
            input: device_query.Device.query.devices
            continue_on_partial_execution: true
            sequential: true
        trigger:
            next:
                - get_device_details
        actions:
            get_device_details:
                id: 6265dc947cc2252f74a5f25261ac36a9
                next:
                    - platform_check
                properties:
                    device_id: "${device_query.Device.query.devices.#}"
                version_constraint: ~1
        conditions:
            platform_check:
                next:
                    - remediate
                expression: get_device_details.Device.GetDetails.Platform:'Windows'
                display:
                    - Platform is Windows
                else:
                    - skip_device
```

**Conditions** use FQL-style `expression:` or CEL `cel_expression:` with optional `else:` for fallback routing:

```yaml
conditions:
    has_detection:
        next:
            - GetDetectionDetails
        cel_expression: data['detection_id'] != null && data['detection_id'] != ''
        display:
            - Detection ID was provided
        else:
            - PrintSummary
```

See [references/workflow-examples.md](references/workflow-examples.md) for full loop and condition examples from production apps.

## The "0" Gotcha

See [pagination-patterns](references/pagination-patterns.md#the-0-gotcha) for the "0" gotcha and fix pattern. Key point: check for both null and `"0"` in loop conditions.

## Workflow Name Uniqueness

Workflow names must be unique across all apps in the same tenant. If two apps deploy workflows with the same name, the second deploy fails silently or produces an "Unknown error." Use app-specific prefixes when the workflow name is generic.

## NEVER Delete and Recreate Workflows

> **⚠️ DANGER:** Deleting a workflow and recreating it with the same name causes cascading failures requiring a fresh app to recover.

Removing a workflow from the manifest does NOT delete its server-side artifact. Recreating with the same name → `409 name must be unique for an app`. A once-failed artifact taints the dependency graph (`400 dependent artifact failed`), blocking all further deploys.

**Instead, update in place** — edit the workflow YAML and redeploy. To re-bind a workflow to a recreated function, update the `id: functions.{name}.{handler}` reference in the YAML. Only delete a workflow if you genuinely no longer need it and won't recreate it with the same name.

## Workflow Sharing

```yaml
workflows:
  - name: my-workflow
    path: workflows/my-workflow/workflow.yaml
    workflow_integration:
      id: <generated-id>
      disruptive: false
      system_action: false   # false = available as a Fusion SOAR response action; true = internal app use only
```

> **⚠️ SOAR action visibility:** `system_action: false` = workflow appears as a Fusion SOAR response action (analysts trigger from detections/incidents). `system_action: true` = internal app use only (scheduled sync, helpers). If the user asks for a "SOAR action" or "response action", use `false`.

## Error Handling

Foundry workflows handle errors through conditional routing and action-level flags — not `onError` blocks or retry middleware. Key mechanisms: `fail_fast_enabled: false` (function actions), `continue_on_partial_execution` (loop blocks), and `conditions:`/`else:` routing. No built-in retry or backoff exists. For the full table, examples, and pagination-polling pattern, see [references/advanced-patterns.md](references/advanced-patterns.md).

## Testing

See [references/advanced-patterns.md](references/advanced-patterns.md) for workflow testing commands (mock triggers, mock actions, execution validation, and `foundry apps validate`).

## Reading Guide

| Task | Reference |
|------|-----------|
| Reading alerts/detections: Event Query vs. source-of-truth API | [references/event-query-vs-api.md](references/event-query-vs-api.md) |
| Full workflow examples (RTR, investigation) | [references/workflow-examples.md](references/workflow-examples.md) |
| Platform action discovery via API | [references/action-discovery.md](references/action-discovery.md) |
| CEL expressions (patterns, has() vs null, extensions) | [references/cel-expressions.md](references/cel-expressions.md) |
| CEL syntax, schemaless queries, dynamic data | [Falcon Fusion SOAR Event Queries: When and How to Go Schemaless](https://www.crowdstrike.com/tech-hub/ng-siem/falcon-fusion-soar-event-queries-when-and-how-to-go-schemaless/) |
| CEL extension functions reference | [Data Transformation Functions](https://docs.crowdstrike.com/r/k223d842) |
| Pagination strategies | [references/pagination-patterns.md](references/pagination-patterns.md) |
| HTTP Actions (call REST APIs without an app) | [references/http-actions.md](references/http-actions.md) |
| Error handling, HTTP Request actions, parameterized fields, counter-rationalizations | [references/advanced-patterns.md](references/advanced-patterns.md) |

## Use Cases

For real-world implementation patterns, see:
- `use-cases/schemaless-queries.md` — CEL expressions, dynamic data, Event Query configuration
- `use-cases/api-pagination.md` — Pagination strategies in functions and workflows
- `use-cases/custom-soar-actions.md` — Custom Falcon Fusion SOAR actions

## Reference Implementations

- **[foundry-sample-threat-intel](https://github.com/CrowdStrike/foundry-sample-threat-intel)**: Threat intelligence workflows with pagination
- **[foundry-sample-rapid-response](https://github.com/CrowdStrike/foundry-sample-rapid-response)**: Rapid response automation
- **[foundry-sample-scalable-rtr](https://github.com/CrowdStrike/foundry-sample-scalable-rtr)**: Scalable RTR orchestration
- **[foundry-sample-ngsiem-importer](https://github.com/CrowdStrike/foundry-sample-ngsiem-importer)**: Threat intel import for NG-SIEM

Referenced files: 8

Package details

Publisher declarations from the archived package. These are separate from our research and the live service's terms.

Package license
MIT
Package author
CrowdStrike
Keywords
crowdstrike, falcon, foundry, cybersecurity, workflows

Declared capabilities

  • Interactive
  • Write

Package observed Oct 2, 2026.

Technical details
First seen
Sep 30, 2026 · 22:02 UTC
Last seen
Oct 2, 2026 · 06:00 UTC
Collection status
Collected

plugins_6a8f6d7f7fac819191e3eba5a7a2e0df

Download plugin data (JSON)