CtrlK
BlogDocsLog inGet started
Tessl Logo

collections-development

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.

72

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Passed

No findings from the security scan

SKILL.md
Quality
Evals
Security

Foundry Collections Development

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

ConstraintRule
Length5-200 characters
Start/endMust begin and end with a letter or number
Special charactersOnly underscores (_) allowed — no hyphens, spaces, or other chars
CaseCase-sensitive

Collection Description Constraints

ConstraintRule
Length3-500 characters
StartMust begin with an alphanumeric character
Allowed charactersLetters, numbers, spaces, dashes, periods, parentheses, and underscores only
Not allowedCommas, colons, semicolons, quotes, slashes, or other special characters

Check the description against this table before running collections create. The CLI accepts a comma (or any other disallowed character) in --description without complaint; the rejection only surfaces later, from foundry apps validate or deploy, by which time the collection is already in the manifest and has to be fixed by hand. Write the description with letters, numbers, spaces, dashes, periods, parentheses, and underscores only.

Collection Limits

ResourceLimit
Single object size~50 MB
Schema size256 KB
Object key length1-1,000 characters
Indexed fields per schema10
Objects per collectionNo enforced limit
Collections per appNo enforced limit
Search results per page500 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

# 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.

Exposing a Collection as an Agent Tool

Add --agent-tools-expose to let a Foundry AI agent read and write the collection:

foundry collections create --name "triage_notes" --schema /tmp/schema.json \
  --description "Agent triage notes" --agent-tools-expose --no-prompt

That writes one block onto the collection's manifest entry:

collections:
    - name: triage_notes
      agent_tools_integration:
        exposed: true

Exposure is only half the wiring. The agent must also name each operation it may call in its own tools list, using collections.<collection_name>.<Operation>:

ai:
    agents:
        - name: Detection Triage Agent
          tools:
            - collections.triage_notes.CreateObject
            - collections.triage_notes.SearchObjects

Valid operations, exact casing required: CreateObject, GetObject, DeleteObject, ListObjects, SearchObjects.

A collection exposed but not listed in tools is unreachable by the agent, and vice versa — neither case produces an error, just an agent that silently cannot use the data. agent_tools_integration is independent of workflow_integration; a collection can be exposed to agents, to Fusion, to both, or to neither.

Agents can also use collections.generic.<Operation> — a scratch collection the platform creates per agent at runtime, needing no collection of your own. Use a named app collection when the data must outlive the agent or be readable by other capabilities. See ai-agents-development.

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

{
  "$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)

{
  "$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

{
  "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

# 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)

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.

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 {}

# Construct inside the handler in a real function — a module-scope client has no request
# token in Foundry and returns 401 on every call (see functions-development).
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"))

# List every key — ListObjects pages by starting key, not a cursor
keys, start = [], None
while True:
    params = {"start": start} if start else {}
    batch = client.ListObjects(collection_name="incidents", limit=100, **params)["body"].get("resources", [])
    batch = [k for k in batch if k != start]  # drop the start key if it comes back
    if not batch:
        break
    keys.extend(batch)
    start = batch[-1]

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"))
  • A missing key returns a 404 (see get_incident in python-patterns.md). Only a 404 means missing; don't treat every non-bytes reply that way: a transient 429 or 5xx then reads as "no record", and code that writes the record back erases it.
  • SearchObjects returns metadata only, not full objects
  • ListObjects returns keys (alphabetical) in body.resources and takes start/end keys. Pass the last key as the next start and stop only on an empty batch — stopping when a page is shorter than limit ends early if the service caps page size
  • List before you read. Calling GetObject on every candidate key (most of them misses) is slow; list the keys once and read only the ones that exist. In one app, that cut a 519-item page load from about 25 s to about 7 s
  • 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.

OperationSyntaxExample
Equalityfield:'value'status:'open'
Numeric comparisonfield:>=Nseverity:>=5
ANDfield1:'a'+field2:'b'status:'open'+severity:>=5
ORfield:'a',field:'b'status:'open',status:'investigating'
Wildcardfield:*'pattern'*title:*'malware'*
Sortingsort=field|ascsort=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:

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
SettingBehavior
workflow_integration.system_action: trueAvailable to app workflows only
workflow_integration.system_action: falseAvailable 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.
  • Commas or other punctuation in --description. collections create accepts them; foundry apps validate and deploy reject them. Stick to letters, numbers, spaces, dashes, periods, parentheses, and underscores.
  • Editing a schema file and redeploying does not change an existing collection. Writes keep validating against the schema the collection was created with (observed: a new top-level field rejected as additional properties not allowed after the schema that declared it was deployed). Ship "additionalProperties": true at the top level if fields may be added later, and treat any breaking schema change as a new collection name.
  • 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

TaskReference
Migrations, testing, pagination, extended schemas, counter-rationalizationsreferences/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: CSV import, bulk operations, pagination workflows, test data generation. See also Getting Started with Falcon Foundry Collections.
Repository
CrowdStrike/foundry-skills
Last updated
First committed

Is this your skill?

If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.