Strategies
Learn once, run forever
What if the AI only needed to figure things out once?
That’s the core insight behind strategies. Most flow steps do the same kind of work every time they run: “this field maps to that parameter,” “parse this instruction into these API params.” The shape of the operation doesn’t change between runs. But calling AI on every execution is slow, expensive, and non-deterministic.
Strategies let you learn once, run forever. First run: AI generates a JavaScript snippet that maps input to output. Every run after: the cached snippet replays instantly. No AI call. Deterministic. Same result every time.
And if the cached snippet ever breaks? The system self-heals — passes the failed code and error back to the AI, which generates a corrected version automatically. You get AI-quality flexibility with hardcoded-speed execution.
Why this matters
Traditional workflow tools solve the schema problem by requiring typed connections between steps. Pipedown takes a different approach — it pipes plain strings between steps and lets AI inside each app figure out what they mean.
That’s powerful, but naive: you don’t want to pay for AI parsing on every single run. A flow that checks Stripe every 15 minutes would make 96 AI calls per day just for parsing.
Consider a step like /stripe payments this week. The Stripe app needs to turn
that natural language into an API call. Without strategies, you have two choices:
- Hardcode parsing — write rules for every possible phrasing. Brittle, incomplete, and every new phrase requires code changes.
- Call AI every time — works for any phrasing, but adds latency and cost to every single run.
Strategies give you the best of both. AI-quality understanding on first run, hardcoded-speed execution on every run after. Flows compose as freely as Unix pipes, handle natural language naturally, and run at near-zero marginal cost after the first execution.
How it works
All three strategy methods share the same core loop:
For object() and list(), the generated JS receives { instruction, summary, data }.
For data(), it receives the raw data directly.
Picking a strategy method
All strategy methods live on tools.strategy and share the same core behavior:
generate a JS snippet on first run, cache it per step, replay it
deterministically on subsequent runs.
Which one you reach for depends on what you’re trying to do.
strategy.object() — parse input into params
The most common method. Use it whenever you need to turn natural language into structured API parameters — which is most of the time.
The generated JS receives { instruction, summary, data } and returns an object
matching the schema:
For simple constant extractions, the generated JS can be as trivial as:
Both work. The JS approach gives the LLM flexibility to generate smarter parsing when the input warrants it.
Options:
| Field | Type | Description |
|---|---|---|
task | string | what the strategy needs to figure out |
schema | object | expected output shape — keys are field names, values are type descriptions |
modelOrTier | ModelOrTier | AI model tier: 'xxs', 'sm', 'md', 'lg' |
forceRegenerate | boolean | regenerate even if a cached strategy exists |
strategy.list() — extract arrays
Same mechanics as object() but the generated JS returns an array. Reach for
this when you’re pulling multiple items out of input — ordered steps, tagged
entities, parsed lists.
strategy.data() — map API responses
This is the heavy lifter for data-intensive apps. It generates a JS function
that transforms raw API responses into typed AppResultDataItem objects the
UI can render automatically — tables, lists, badges, links all work without
per-app rendering code.
The really cool part: the app developer doesn’t write any mapping code. On first run, the AI sees a sample of the raw data and generates a complete mapping function.
Options:
| Field | Type | Description |
|---|---|---|
data | unknown | raw API response data |
task | string | what this data represents (e.g. “stripe payments”) |
fields | string[] | fields to prioritize in summary and mapping |
modelOrTier | ModelOrTier | AI model tier |
forceRegenerate | boolean | regenerate even if cached |
maxRetries | number | retry attempts if generated code fails (default: 2) |
Return shape:
Each item conforms to AppResultDataItem — a shape with well-defined fields
the UI knows how to render:
Here’s what a generated JS snippet looks like for Stripe payments:
strategy.fetch() — params + request in one shot
A common pattern: use a strategy to figure out API parameters, then immediately
fetch data with them. strategy.fetch() combines both steps so you don’t have
to wire them together yourself.
Internally it uses strategy.object() to resolve the parameters (cached), then
calls your fetch function every run.
Schemas.Data
A preset schema matching AppResultDataItem, exported from start/app so apps
can reference the standard data shape:
Self-healing
This is one of the genuinely exciting parts. Strategies don’t just cache — they recover from failures automatically at two levels.
Method-level recovery
Built into every strategy method. When cached code fails, the system doesn’t just throw — it iterates:
- Execute the cached JS snippet
- If it throws, regenerate with the error context AND the failed code
- Retry (up to 2 attempts by default, configurable for
data()) - Validate return shape matches schema if provided
- Cache the working version
The key insight: on retry, the LLM sees both the error AND its own previous code. It can fix the specific bug instead of starting from scratch. An API changes its response shape? The strategy adapts on the next run.
The retry prompt looks like:
Error: TypeError: Cannot read property ‘amount’ of undefined
Fix the error. The data shape may differ from what you assumed.
This pattern appears throughout the built-in apps. The Stripe app caches
{ period: "week" } and computes created.gte / created.lte from period at
runtime. The GitHub app caches { since: "last-7-days" } and resolves the ISO
date when the action runs.
Think of it this way: the strategy captures what to do. The app code handles when.
Key format hints
When an app has multiple data paths (sync, actions, webhooks), they should all
write the same keys for the same entities. Otherwise three separate strategy runs
might generate slightly different key formats for the same data. Pass
keyFormats in hints to constrain the generated JS:
Now /github issues (action), hourly sync, and webhook events all write to
github-issue:42 for issue #42.
Key format hints are extracted from hints and surfaced as a hard constraint in
the prompt. Other hints fields are passed as general context.
Invalidation
Strategies are cached per step index within a flow. They can be invalidated:
- Per-call: pass
forceRegenerate: trueto regenerate - Per-step: call
tools.strategy.clear()in app code - Per-flow: the flow UI has a button to clear all strategies
In practice, strategies rarely need manual invalidation. They capture the shape of the operation, not the data — so they stay correct as long as the flow’s structure doesn’t change. Self-healing handles most runtime failures automatically.