@step reads through ctx.duckdb. Joins, aggregations, and correlations happen at native speed in columnar storage — no LLM, no per-row Python loops. Strategies return structured results.
A strategy lives as three files: contract.yaml (what data it needs), strategy.py (the DAG of steps), and an optional binding.yaml (how to fetch the data in your environment). The first two are publishable and portable; the binding plugs the strategy into your specific stack.
In this section
- Quick Start — build a minimal end-to-end strategy with all three files
- Portability — why
contract.yaml+strategy.pyare separate frombinding.yaml - Contracts — full
contract.yamlschema reference - Bindings — full
binding.yamlschema, fetch modes, pagination - Response Adapters — parsing non-JSON tool responses
- Lifecycle — discovery, hot reload, governance status
Authoring Paths
There are two supported ways to create strategies:- Manual filesystem authoring — create
contract.yaml,strategy.py, and optionalbinding.yamlunderstrategies/<domain>/<category>/<slug>/(e.g.strategies/security/enrichment/splunk_field_survey/). - MCP-driven creation — call
strategy_createwith Python code plus either:contract(preferred, YAML string forcontract.yaml)metadata(legacy JSON path)
strategy_create writes the strategy files through the sidecar and can also register governance nodes in the graph when the graph is connected.
Use manual authoring when iterating locally in the repo. Use strategy_create when an agent or operator is compiling exploratory work into a reusable strategy through fracta itself.
Directory Layout
strategies/ recursively on every call and picks up any directory with both contract.yaml and strategy.py. Nesting depth is up to you — the runner uses os.walk, so strategies/security/enrichment/splunk_field_survey/ works the same as a flatter strategies/enrichment/splunk_field_survey/. See Lifecycle for the full discovery and hot-reload rules.
The convention is domain first, then category. <domain> is your top-level grouping (e.g. security, infra, finance); <category> is the strategy type within that domain. The directory layout is purely organizational — a strategy’s identity is the name field in its contract.yaml, not its path.
Contract Reference
Thecontract.yaml defines what the strategy does, what parameters it accepts, and what data it needs.
Required Fields
Parameters
Data Requirements
VARCHAR, INTEGER, BIGINT, DOUBLE, TIMESTAMP, BOOLEAN, etc.
Semantic tags: Optional hints that enable the auto-resolve pipeline to match columns to MCP backend fields. Without semantic tags, a binding.yaml is required for auto-staging.
Backend Pinning and Discovery Hints
pinned_backend tells the resolver which MCP backend to use. mcp_hints are advisory — they guide the orchestrator and auto-resolve pipeline.
Binding Reference
The optionalbinding.yaml maps contract table requirements to concrete MCP data sources. Required when columns lack semantic tags for auto-resolve.
Fetch Modes
| Mode | Who fetches | When | Use case |
|---|---|---|---|
fracta_mcp_gateway | Go (MCP client pool) | Inline at strategy_run time | Default for gateway-connected backends. Auto-staged. |
mcp | Agent (via strategy_stage) | Agent calls tool, stages result | When agent must orchestrate the fetch (e.g., multi-step queries) |
native | Strategy Python code | At runtime via ctx.graph or ctx.duckdb | Graph-only strategies that populate tables themselves |
Query Templates
Templates use{{param_name}} placeholders resolved from strategy params:
Field Mapping
field_map renames source fields to match contract column names:
Pagination (for large tables)
fracta_mcp_gateway fetches. The current heuristic is:
paginationis configured- and
max_rows > 50000
{"status": "staging"} and can poll with the session_id.
Python Framework
Strategy Base Class
Every strategy is a Python class that extendsStrategy:
@step Decorator
Marks a method as a pipeline step. Steps run in topologically sorted order.depends:
StrategyContext
Every step receivesctx with:
| Field | Type | Description |
|---|---|---|
ctx.duckdb | duckdb.Connection | Fresh per-run DuckDB (400MB memory limit, spill to disk). Staged tables are pre-loaded. |
ctx.graph | FalkorDB client or None | Knowledge graph access. Only available when requires.graph: true and graph is configured. |
ctx.params | dict | Validated and type-coerced parameters from the caller. |
ctx.mcp | MCPGatewayClient or None | Gateway client for mid-execution MCP tool calls. Available when the gateway is configured and strategy.gateway_access: true. See Runtime API. |
Querying Staged Data
Tables declared inrequires.tables are loaded into DuckDB from Parquet before execution:
Querying the Knowledge Graph
Whenrequires.graph: true:
Mid-Execution Tool Calls (ctx.mcp)
When the gateway is configured andstrategy.gateway_access: true, strategies can call MCP tools during execution for targeted follow-up queries:
ctx.mcp vs pre-staging:
Use ctx.mcp when… | Use pre-staging when… |
|---|---|
| Follow-up queries depend on intermediate results | Data requirements are known upfront |
| Fetching targeted data for a small set of IOCs | Bulk data needed for joins/aggregations |
| The query can’t be formulated until mid-execution | DuckDB columnar processing is the goal |
Data Flow
DuckDB is the engine
DuckDB is a core construct of the strategies framework, not an implementation detail. Every strategy run gets a fresh in-process DuckDB instance (400 MB memory, spill-to-disk enabled). All staged data lands in that DuckDB as tables. Every@step reads through ctx.duckdb. The Python in strategy.py is essentially orchestration around DuckDB queries — joins, aggregations, window functions, and CTEs run at native speed against columnar storage, not as Python loops over dicts.
This is what makes strategies cheap and deterministic. A correlation across two ten-million-row tables is a SQL join in DuckDB — milliseconds, no LLM, identical answer every time.
How data gets into DuckDB
The fetch mode declared inbinding.yaml decides who pulls the data. The choice has direct cost implications:
The three fetch modes:
| Mode | Who fetches | LLM in the loop? | When to use |
|---|---|---|---|
fracta_mcp_gateway | Go gateway, via the MCP client pool | No — the gateway pulls bytes straight into Parquet | Default for any backend reachable through the gateway. This is the token-saver: the agent calls strategy_run once, the gateway does the work, and the LLM only ever sees the strategy’s structured result. |
mcp | The agent itself, via MCP tools | Yes — the agent calls the tool, gets the response, and stages it via strategy_stage | When the fetch needs LLM judgement: multi-step queries, conditional follow-ups, or backends only accessible through the agent’s MCP routing. |
native | The strategy’s Python code at runtime | No | Graph-only strategies, or strategies that compute their own tables (e.g. cross-joins of other staged tables, synthetic enrichments). |
fracta_mcp_gateway and joins them in DuckDB costs the same in tokens whether it returns ten rows or zero — because the LLM only ever sees the final summary.
The runtime sequence
MCP Tools
Agents interact with strategies through these MCP tools:| Tool | Purpose |
|---|---|
strategy_list | List all discovered strategies. When the graph is connected, results include governance status. |
strategy_describe | Get full details for a strategy including resolution plan |
strategy_run | Execute a strategy. Auto-resolves data, runs steps, returns result. |
strategy_create | Create a new strategy from Python code plus contract YAML (preferred) or legacy metadata JSON |
strategy_stage | Manually stage MCP results as Parquet (for fetch_mode: mcp) |
strategy_stage_status | Query persistent staging progress for a run (session_id) |
strategy_resolve | Preview the resolution plan without executing |
strategy_match | Find strategies matching an investigation intent (scored) |
strategy_promote | Advance a strategy from validated to promoted status |
strategy_list does not filter by status by default; pass status="validated,promoted" (or "exploratory", "all") to filter. See Lifecycle for the governance-status model.
strategy_run Response States
| Status | Meaning | Next action |
|---|---|---|
complete | Strategy executed successfully | Read result and trace |
error | Strategy failed | Read error and structured_error |
pending | Agent must stage data | Call strategy_stage for each pending table, then retry with session_id |
staging | Background staging in progress | Retry with session_id to poll or get result |
executing | Another caller is running this session | Wait and retry with session_id |
Error Handling
Partial Results
When a step fails, all previously completed step outputs are preserved:Structured Errors
Strategy errors include classification for client-side handling:transient (retryable), permanent (fix required), validation (bad input), partial (strategy ran but incomplete).
Deployment
Docker Image
Strategies are baked into the Docker image at/opt/fracta/strategies/. The Dockerfile copies the strategies/ directory:
strategy-runner and fracta-gateway, see Lifecycle.
