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.
83
82%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
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.
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.
| 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 |
draft/2020-12, draft/2019-09) fail validationadditionalProperties: false recommended — extra fields leak internal data and break type safetyx-cs-indexable: true on individual properties for searchable fields (max 10 per collection)# 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.
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){
"$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" }
}
}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.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.
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' }],
});
}
}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 functions-development/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 {}
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 objectsx-cs-indexable: true in the collection schemaOnly 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.
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| 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 |
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.
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.x-cs-indexable: true or listed in x-cs-indexable-fields. Max 10 per collection.workflow_integration.system_action: true for app-only workflow access, or false to also expose collections as Falcon Fusion SOAR actions.foundry-js SDK.| Task | Reference |
|---|---|
| Migrations, testing, pagination, extended schemas, counter-rationalizations | references/advanced-patterns.md |
For real-world implementation patterns, see:
e7fa026
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.