Pipe recipes

Small programs built from existing commands. Open a recipe in Pipe, change its input or options, then choose Run.

Each recipe includes sample input, command options, a curl request, expected output, and the missing operations it reveals. Opening this page or loading a recipe makes no outgoing network calls and creates no datasets. Network recipes make the listed calls only when run.

Clean and deduplicate a contact CSV

Trim names, normalize email spelling, discard blank email addresses, and retain one row per email.

parse → replace → replace → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": "name,email,team\n Alice ,[email protected] ,Ops\nBob,[email protected],Support\nAlice,[email protected],Ops\nCara,,Ops\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "replace",
      "options": {
        "field": "name",
        "pattern": "^\\s+|\\s+$",
        "replacement": ""
      }
    },
    {
      "command": "replace",
      "options": {
        "field": "email",
        "pattern": "\\s+",
        "replacement": ""
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map(.email |= ascii_downcase) | map(select(.email != \"\")) | unique_by(.email) | sort_by(.name)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": "name,email,team\n Alice ,[email protected] ,Ops\nBob,[email protected],Support\nAlice,[email protected],Ops\nCara,,Ops\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "replace",
      "options": {
        "field": "name",
        "pattern": "^\\s+|\\s+$",
        "replacement": ""
      }
    },
    {
      "command": "replace",
      "options": {
        "field": "email",
        "pattern": "\\s+",
        "replacement": ""
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map(.email |= ascii_downcase) | map(select(.email != \"\")) | unique_by(.email) | sort_by(.name)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}'
Expected output for this input
name,email,team
Alice,[email protected],Ops
Bob,[email protected],Support

What this exposes: Trim, case conversion, distinct-by-field, and sort could become short named steps. Email normalization here is a chosen policy, not address validation.

Count warnings and errors by service

Keep warning/error lines, replace one known token pattern, and produce a Markdown summary.

grep → replace → parse → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": "INFO api started\nERROR api timeout token=sample-123\nWARN worker retry\nERROR api unavailable\n",
  "steps": [
    {
      "command": "grep",
      "options": {
        "pattern": "^(ERROR|WARN) "
      }
    },
    {
      "command": "replace",
      "options": {
        "pattern": "token=\\S+",
        "replacement": "token=[redacted]"
      }
    },
    {
      "command": "parse",
      "options": {
        "format": "lines"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map(capture(\"^(?<level>[A-Z]+) (?<service>[a-z]+) (?<message>.*)$\")) | group_by(.service) | map({service: .[0].service, warnings: (map(select(.level == \"WARN\")) | length), errors: (map(select(.level == \"ERROR\")) | length)})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": "INFO api started\nERROR api timeout token=sample-123\nWARN worker retry\nERROR api unavailable\n",
  "steps": [
    {
      "command": "grep",
      "options": {
        "pattern": "^(ERROR|WARN) "
      }
    },
    {
      "command": "replace",
      "options": {
        "pattern": "token=\\S+",
        "replacement": "token=[redacted]"
      }
    },
    {
      "command": "parse",
      "options": {
        "format": "lines"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map(capture(\"^(?<level>[A-Z]+) (?<service>[a-z]+) (?<message>.*)$\")) | group_by(.service) | map({service: .[0].service, warnings: (map(select(.level == \"WARN\")) | length), errors: (map(select(.level == \"ERROR\")) | length)})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}'
Expected output for this input
| service | warnings | errors |
| --- | --- | --- |
| api | 0 | 2 |
| worker | 1 | 0 |

What this exposes: Group and aggregate would shorten the jq step. Pattern replacement only removes the pattern shown; it is not a general secret scrubber.

Total paid orders by customer

Parse CSV quantities and integer unit prices, ignore cancelled orders, and total each customer in cents.

parse → grep → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": "customer,qty,unit_cents,status\nAlice,2,125,paid\nBob,1,500,paid\nAlice,1,300,paid\nBob,9,500,cancelled\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "grep",
      "options": {
        "field": "status",
        "pattern": "^paid$"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({customer, total_cents: ((.qty | tonumber) * (.unit_cents | tonumber))}) | group_by(.customer) | map({customer: .[0].customer, total_cents: (map(.total_cents) | add)}) | sort_by(.customer)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": "customer,qty,unit_cents,status\nAlice,2,125,paid\nBob,1,500,paid\nAlice,1,300,paid\nBob,9,500,cancelled\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "grep",
      "options": {
        "field": "status",
        "pattern": "^paid$"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({customer, total_cents: ((.qty | tonumber) * (.unit_cents | tonumber))}) | group_by(.customer) | map({customer: .[0].customer, total_cents: (map(.total_cents) | add)}) | sort_by(.customer)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}'
Expected output for this input
customer,total_cents
Alice,550
Bob,500

What this exposes: Typed parsing, grouping, and reduction are recurring needs. Integer cents avoid binary-fraction rounding in this small example; production amounts still need range and currency rules.

Compare two inventory snapshots

Full-join records by SKU, calculate quantity changes, and include additions and removals.

join → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    {
      "sku": "A",
      "quantity": 5
    },
    {
      "sku": "B",
      "quantity": 3
    },
    {
      "sku": "D",
      "quantity": 2
    }
  ],
  "steps": [
    {
      "command": "join",
      "options": {
        "other": [
          {
            "sku": "A",
            "quantity": 8
          },
          {
            "sku": "C",
            "quantity": 4
          },
          {
            "sku": "D",
            "quantity": 2
          }
        ],
        "left_key": "sku",
        "right_key": "sku",
        "how": "full"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({sku: (.left.sku // .right.sku), before: (.left.quantity // 0), after: (.right.quantity // 0), kind: (if .left == null then \"added\" elif .right == null then \"removed\" else \"updated\" end)}) | map(. + {delta: (.after - .before)}) | map(select(.delta != 0 or .kind != \"updated\")) | sort_by(.sku)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    {
      "sku": "A",
      "quantity": 5
    },
    {
      "sku": "B",
      "quantity": 3
    },
    {
      "sku": "D",
      "quantity": 2
    }
  ],
  "steps": [
    {
      "command": "join",
      "options": {
        "other": [
          {
            "sku": "A",
            "quantity": 8
          },
          {
            "sku": "C",
            "quantity": 4
          },
          {
            "sku": "D",
            "quantity": 2
          }
        ],
        "left_key": "sku",
        "right_key": "sku",
        "how": "full"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({sku: (.left.sku // .right.sku), before: (.left.quantity // 0), after: (.right.quantity // 0), kind: (if .left == null then \"added\" elif .right == null then \"removed\" else \"updated\" end)}) | map(. + {delta: (.after - .before)}) | map(select(.delta != 0 or .kind != \"updated\")) | sort_by(.sku)"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}'
Expected output for this input
sku,before,after,kind,delta
A,5,8,updated,3
B,3,0,removed,'-3
C,0,4,added,4

What this exposes: A keyed record-diff helper could expose added/removed/changed directly. Duplicate SKUs create join combinations, so validate uniqueness first. CSV prefixes negative-looking cells for spreadsheet safety; the JSON result before Format retains numeric deltas.

Find added and removed members

Compare two lists as sets and turn the differences into a short change report.

comm → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    "Ada",
    "Ben",
    "Cara"
  ],
  "steps": [
    {
      "command": "comm",
      "options": {
        "other": [
          "Ben",
          "Cara",
          "Dev"
        ]
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "[.only_left[] | {name: ., change: \"removed\"}] + [.only_right[] | {name: ., change: \"added\"}]"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    "Ada",
    "Ben",
    "Cara"
  ],
  "steps": [
    {
      "command": "comm",
      "options": {
        "other": [
          "Ben",
          "Cara",
          "Dev"
        ]
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "[.only_left[] | {name: ., change: \"removed\"}] + [.only_right[] | {name: ., change: \"added\"}]"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}'
Expected output for this input
| name | change |
| --- | --- |
| Ada | removed |
| Dev | added |

What this exposes: Comm already handles set membership. It does not preserve duplicate counts or compare changed fields on matching IDs; those are different operations.

Prepare review tasks from a CSV

Keep high-priority documents and build newline-delimited JSON task payloads for another program to submit.

parse → grep → jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": "document_id,url,priority\ndoc-1,https://example.com/a,high\ndoc-2,https://example.com/b,low\ndoc-3,https://example.com/c,urgent\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "grep",
      "options": {
        "field": "priority",
        "pattern": "^(high|urgent)$"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({kind: \"document.review\", document_id, url, priority})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "ndjson"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": "document_id,url,priority\ndoc-1,https://example.com/a,high\ndoc-2,https://example.com/b,low\ndoc-3,https://example.com/c,urgent\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "csv"
      }
    },
    {
      "command": "grep",
      "options": {
        "field": "priority",
        "pattern": "^(high|urgent)$"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({kind: \"document.review\", document_id, url, priority})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "ndjson"
      }
    }
  ]
}'
Expected output for this input
{"kind":"document.review","document_id":"doc-1","url":"https://example.com/a","priority":"high"}
{"kind":"document.review","document_id":"doc-3","url":"https://example.com/c","priority":"urgent"}

What this exposes: There is no per-record Queue write in Pipe. A final queue.push enqueues the entire preceding result as one item. A future explicit each-write operation needs per-item keys, budgets, and partial-failure rules.

Make a stable DNS snapshot

Look up public A records and compare sorted addresses rather than changing TTL values or answer order.

dig → jq → format

One public DNS lookup. Live addresses vary; the displayed output uses a test fixture.

Input and command options
{
  "input": null,
  "steps": [
    {
      "command": "dig",
      "options": {
        "name": "example.com",
        "type": "A"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "if .status != 0 then error(\"DNS lookup failed\") else [.answers[] | select(.type == 1) | .value] | unique | sort end"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "lines"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": null,
  "steps": [
    {
      "command": "dig",
      "options": {
        "name": "example.com",
        "type": "A"
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "if .status != 0 then error(\"DNS lookup failed\") else [.answers[] | select(.type == 1) | .value] | unique | sort end"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "lines"
      }
    }
  ]
}'
Fixture output; live results vary
192.0.2.10
192.0.2.20

What this exposes: Append Diff plus conditional Notify to announce changes. This is a fixed resolver view, not authoritative DNS propagation coverage. DNS failures are errors here, not empty snapshots.

Check a small list of public URLs

Use Xargs to issue a HEAD request for each URL and report status, duration, and errors.

xargs → jq → format

Two public HTTP HEAD requests. Live status and timing vary; the displayed output uses test fixtures.

Input and command options
{
  "input": [
    "https://example.com",
    "https://www.jsoftware.com"
  ],
  "steps": [
    {
      "command": "xargs",
      "options": {
        "steps": [
          {
            "command": "fetch",
            "options": {
              "method": "HEAD"
            }
          }
        ]
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({url, status: (.status // \"error\"), duration_ms, error: (.error // \"\")})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    "https://example.com",
    "https://www.jsoftware.com"
  ],
  "steps": [
    {
      "command": "xargs",
      "options": {
        "steps": [
          {
            "command": "fetch",
            "options": {
              "method": "HEAD"
            }
          }
        ]
      }
    },
    {
      "command": "jq",
      "options": {
        "expression": "map({url, status: (.status // \"error\"), duration_ms, error: (.error // \"\")})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}'
Fixture output; live results vary
| url | status | duration_ms | error |
| --- | --- | --- | --- |
| https://example.com | 200 | 12 |  |
| https://www.jsoftware.com | 503 | 30 |  |

What this exposes: Xargs supplies each URL string to Fetch when url is omitted. It runs sequentially, at most 20 records. General typed binding of earlier outputs to later options is still missing.

Calculate a total, count, and mean

Reuse the same number list for several calculations: the shape behind a J fork.

jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    12,
    16,
    20
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "{total: (add // 0), count: length, mean: (if length == 0 then null else add / length end)}"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "json"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    12,
    16,
    20
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "{total: (add // 0), count: length, mean: (if length == 0 then null else add / length end)}"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "json"
      }
    }
  ]
}'
Expected output for this input
{
  "total": 48,
  "count": 3,
  "mean": 16
}

What this exposes: J can express a mean with the fork (+/ % #). Today jq carries this composition. A future Fork could give the same input to named total/count branches and combine their outputs.

Calculate each project’s share of a total

Keep the original rows while deriving a shared total, then add a percentage to each row.

jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    {
      "project": "A",
      "hours": 2
    },
    {
      "project": "B",
      "hours": 6
    }
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "(map(.hours) | add // 0) as $total | map(. + {percent: (if $total == 0 then 0 else .hours * 100 / $total end)})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    {
      "project": "A",
      "hours": 2
    },
    {
      "project": "B",
      "hours": 6
    }
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "(map(.hours) | add // 0) as $total | map(. + {percent: (if $total == 0 then 0 else .hours * 100 / $total end)})"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "csv"
      }
    }
  ]
}'
Expected output for this input
project,hours,percent
A,2,25
B,6,75

What this exposes: This has a hook-like shape: combine original rows with a value derived from them. A Fork with one identity branch could cover it without a separate Hook command. Zero total produces zero percentages by explicit policy.

Calculate a running balance

Carry a total through an ordered list of changes and retain each intermediate balance.

jq → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    {
      "day": "Mon",
      "change": 3
    },
    {
      "day": "Tue",
      "change": 4
    },
    {
      "day": "Wed",
      "change": -2
    },
    {
      "day": "Thu",
      "change": 5
    }
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "reduce .[] as $row ({total: 0, rows: []}; .total += $row.change | .rows += [$row + {balance: .total}]) | .rows"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "json"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    {
      "day": "Mon",
      "change": 3
    },
    {
      "day": "Tue",
      "change": 4
    },
    {
      "day": "Wed",
      "change": -2
    },
    {
      "day": "Thu",
      "change": 5
    }
  ],
  "steps": [
    {
      "command": "jq",
      "options": {
        "expression": "reduce .[] as $row ({total: 0, rows: []}; .total += $row.change | .rows += [$row + {balance: .total}]) | .rows"
      }
    },
    {
      "command": "format",
      "options": {
        "format": "json"
      }
    }
  ]
}'
Expected output for this input
[
  {
    "day": "Mon",
    "change": 3,
    "balance": 3
  },
  {
    "day": "Tue",
    "change": 4,
    "balance": 7
  },
  {
    "day": "Wed",
    "change": -2,
    "balance": 5
  },
  {
    "day": "Thu",
    "change": 5,
    "balance": 10
  }
]

What this exposes: Scan is a strong candidate: reduce returns the final accumulator; scan exposes intermediate accumulators. This works within one request; Window accumulates across separate requests.

Choose a parser from the input format

Select a parser branch, then format its result using a shared following step.

choose → format

Sample input · no network requests · no saved output

Input and command options
{
  "input": {
    "format": "csv",
    "body": "name,quantity\nPencil,3\nNotebook,2\n"
  },
  "steps": [
    {
      "command": "choose",
      "options": {
        "select": ".format",
        "cases": {
          "csv": [
            {
              "command": "jq",
              "options": {
                "expression": ".body"
              }
            },
            {
              "command": "parse",
              "options": {
                "format": "csv"
              }
            }
          ],
          "json": [
            {
              "command": "jq",
              "options": {
                "expression": ".body"
              }
            },
            {
              "command": "parse",
              "options": {
                "format": "json"
              }
            }
          ]
        }
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": {
    "format": "csv",
    "body": "name,quantity\nPencil,3\nNotebook,2\n"
  },
  "steps": [
    {
      "command": "choose",
      "options": {
        "select": ".format",
        "cases": {
          "csv": [
            {
              "command": "jq",
              "options": {
                "expression": ".body"
              }
            },
            {
              "command": "parse",
              "options": {
                "format": "csv"
              }
            }
          ],
          "json": [
            {
              "command": "jq",
              "options": {
                "expression": ".body"
              }
            },
            {
              "command": "parse",
              "options": {
                "format": "json"
              }
            }
          ]
        }
      }
    },
    {
      "command": "format",
      "options": {
        "format": "markdown"
      }
    }
  ]
}'
Expected output for this input
| name | quantity |
| --- | --- |
| Pencil | 3 |
| Notebook | 2 |

What this exposes: Choose runs only the selected branch. Unsupported formats fail explicitly because this recipe has no fallback.

Normalize different event types

Use Choose inside Xargs to transform each event, retaining one ordered output list.

xargs

Sample input · no network requests · no saved output

Input and command options
{
  "input": [
    {
      "kind": "order",
      "id": "A",
      "quantity": 3,
      "price": 7
    },
    {
      "kind": "heartbeat",
      "worker": "reader"
    },
    {
      "kind": "other",
      "value": 2
    }
  ],
  "steps": [
    {
      "command": "xargs",
      "options": {
        "steps": [
          {
            "command": "choose",
            "options": {
              "select": ".kind",
              "cases": {
                "order": [
                  {
                    "command": "jq",
                    "options": {
                      "expression": "{kind, id, total:(.quantity*.price)}"
                    }
                  }
                ],
                "heartbeat": [
                  {
                    "command": "jq",
                    "options": {
                      "expression": "{kind, worker, checked_in:true}"
                    }
                  }
                ]
              },
              "otherwise": []
            }
          }
        ]
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": [
    {
      "kind": "order",
      "id": "A",
      "quantity": 3,
      "price": 7
    },
    {
      "kind": "heartbeat",
      "worker": "reader"
    },
    {
      "kind": "other",
      "value": 2
    }
  ],
  "steps": [
    {
      "command": "xargs",
      "options": {
        "steps": [
          {
            "command": "choose",
            "options": {
              "select": ".kind",
              "cases": {
                "order": [
                  {
                    "command": "jq",
                    "options": {
                      "expression": "{kind, id, total:(.quantity*.price)}"
                    }
                  }
                ],
                "heartbeat": [
                  {
                    "command": "jq",
                    "options": {
                      "expression": "{kind, worker, checked_in:true}"
                    }
                  }
                ]
              },
              "otherwise": []
            }
          }
        ]
      }
    }
  ]
}'
Expected output for this input
[
  {
    "kind": "order",
    "id": "A",
    "total": 21
  },
  {
    "kind": "heartbeat",
    "worker": "reader",
    "checked_in": true
  },
  {
    "kind": "other",
    "value": 2
  }
]

What this exposes: Choose changes individual records. Xargs preserves their order and accepts at most 20 records. The empty fallback keeps unmatched events unchanged.

Split and summarize three event streams

Read NDJSON, group records by event kind, and process each populated group once.

parse → route

Sample input · no network requests · no saved output

Input and command options
{
  "input": "{\"kind\":\"error\",\"service\":\"api\"}\n{\"kind\":\"measurement\",\"value\":12}\n{\"kind\":\"job\",\"id\":\"A\"}\n{\"kind\":\"error\",\"service\":\"api\"}\n{\"kind\":\"measurement\",\"value\":18}\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "ndjson"
      }
    },
    {
      "command": "route",
      "options": {
        "select": ".kind",
        "cases": {
          "error": [
            {
              "command": "jq",
              "options": {
                "expression": "group_by(.service) | map({service:.[0].service,count:length})"
              }
            }
          ],
          "measurement": [
            {
              "command": "jq",
              "options": {
                "expression": "{count:length,average:(map(.value)|add/length)}"
              }
            }
          ],
          "job": [
            {
              "command": "jq",
              "options": {
                "expression": "map({task_id:.id,action:\"review\"})"
              }
            }
          ]
        },
        "otherwise": []
      }
    }
  ]
}
Curl request
curl -sS https://bay.farm/api/commands/pipe \
  -H 'Content-Type: application/json' \
  --data-binary '{
  "input": "{\"kind\":\"error\",\"service\":\"api\"}\n{\"kind\":\"measurement\",\"value\":12}\n{\"kind\":\"job\",\"id\":\"A\"}\n{\"kind\":\"error\",\"service\":\"api\"}\n{\"kind\":\"measurement\",\"value\":18}\n",
  "steps": [
    {
      "command": "parse",
      "options": {
        "format": "ndjson"
      }
    },
    {
      "command": "route",
      "options": {
        "select": ".kind",
        "cases": {
          "error": [
            {
              "command": "jq",
              "options": {
                "expression": "group_by(.service) | map({service:.[0].service,count:length})"
              }
            }
          ],
          "measurement": [
            {
              "command": "jq",
              "options": {
                "expression": "{count:length,average:(map(.value)|add/length)}"
              }
            }
          ],
          "job": [
            {
              "command": "jq",
              "options": {
                "expression": "map({task_id:.id,action:\"review\"})"
              }
            }
          ]
        },
        "otherwise": []
      }
    }
  ]
}'
Expected output for this input
{
  "error": [
    {
      "service": "api",
      "count": 2
    }
  ],
  "measurement": {
    "count": 2,
    "average": 15
  },
  "job": [
    {
      "task_id": "A",
      "action": "review"
    }
  ],
  "otherwise": []
}

What this exposes: Route processes whole groups, so aggregation and batching are possible. Output is named branch results. Separate live streams and writes to several destinations are not implemented; one final Pipe action can save this combined result.

What J contributes

J composes functions using rules for hooks, forks, and longer trains. The rules describe how inputs reach functions; they are not simply a list of operations executed in order. Bay Farm can borrow these data-flow patterns while keeping explicit JSON and ordinary command names.

CompositionOne-input formBay Farm interpretation
Pipeh(g(f(x)))Each step receives only the preceding output. Implemented.
ChooseSelect one branch for the current valueImplemented. Inspired by J’s gerunds and Agenda selection.
RoutePartition records, then process each groupImplemented for finite batches. Each record enters one named group.
J fork(f g h) y = (f y) g (h y)Give one input to two branches, then combine their results. Proposed.
J hook(f g) y = y f (g y)Combine the original input with a derived value. Proposed as a fork with an identity branch.
J trainA composition interpreted through hook/fork rulesA broader composition concept. There is no need for a third execution service.

For example, J’s mean (+/ % #) divides a sum by a count from the same list. The shared-summary recipe does that today with jq. The normalize-shares recipe keeps rows while deriving their total. J’s hook and fork definitions.

One list enters sum and count branches; divide combines their results to produce the mean.
A proposed value fork. Branches could run sequentially; this does not imply process creation or parallel requests.
Original rows and their derived total enter a combination that adds a percentage to every row.
A hook-like calculation can use a fork with an identity branch.

Conditional branches and grouped streams

J gerunds and Agenda offer a model for selecting functions. Bay Farm uses named cases: Choose selects one branch for the current value; Route selects a group for each record and passes each populated group to its branch once. Fork would instead give the same input to every branch.

Records split by kind into error, measurement and job groups. Each group reaches its own processing branch.
Route creates disjoint groups. Each downstream branch receives an array, enabling batching and aggregation.

See parser selection, conditional record transformations and three routed event groups. An empty branch passes its input through. Unmatched selectors use an explicit otherwise branch or fail. An empty Route group returns [] without invoking its commands. Selected branch errors stop the run; they do not invoke the fallback.

Choose can operate on any JSON value; Route takes up to 1000 records. All branches share the run’s operation, network, byte and time limits. Dataset writes stay outside branches as an explicit final Pipe action. Route handles a finite batch; it does not create persistent processes, separate live URLs or several independent delivery transactions.

A possible Fork request shape

Design proposal only. The current API does not accept fork, hook, or train commands. Named branches would make intermediate values visible. Empty branches could mean identity. Begin with a small branch count, pure steps, and one shared execution budget; allow no dataset writes inside branches.

{
  "command": "fork",
  "options": {
    "branches": {
      "total": [{"command":"jq","options":{"expression":"add // 0"}}],
      "count": [{"command":"jq","options":{"expression":"length"}}]
    },
    "combine": [{"command":"jq","options":{
      "expression":"if .count == 0 then null else .total / .count end"
    }}]
  }
}

This would be a JSON composition operator inspired by J, not an implementation of J syntax, ranks, or its full train grammar. Here a composition hook means combining original and derived values; HTTP webhooks continue to be Notify receivers.

J also provides useful models for reduction, prefix scan, grouping by keys, and sorting by an ordering. The recipes use jq for these jobs today. New names should shorten recurring programs, not require people to learn another symbolic language.

What the recipes show is missing

These are proposed additions or interface improvements, not commands available in the examples above.

CandidateWhy it helpsCurrent workaround or boundary
Fork with named branchesReuse one input, inspect intermediate results, and combine analyses.jq variables and object expressions. A Hook can be represented with an identity branch; defer a separate Train command.
Assert / requireStop with a clear failure for a bad response or invalid record.jq error(...). when short-circuits and returns the current value; it does not fail validation or return an empty result.
ScanRunning totals, balances, streaks, and intermediate state within one input list.A jq reduce that collects accumulators. Window instead keeps state across requests.
Typed option bindingUse a value produced earlier as a later command’s URL, selected field, or threshold.Options are literal. Xargs has a narrow URL-string → Fetch / name → Dig convention. General binding needs explicit selectors and type checking.
Group and reduceExpress counts, totals, extrema, and grouped reports with shorter steps.jq already supports these calculations. Add small named operations only where repeated recipes justify them.
Sort / take / distinctBuild rankings and clean lists without repeated jq expressions.jq sort_by, array slices, and unique_by. Define stable order, missing values, and duplicate selection.
Zip / transposeAlign parallel lists and convert row-oriented data to columns.jq expressions. Decide how unequal lengths and ragged arrays behave before exposing an API.
URL parse / resolveTurn extracted relative links into complete addresses.Resolve in the caller. All subsequent Fetch calls must retain the current public-network checks.
Explicit per-item writesCreate one queue task or record for each result.Xargs permits transformations and bounded reads only; one final queue.push writes one payload. A write iterator needs per-item idempotency, quotas, and partial-failure reporting.

Choose and Route now cover conditional processing. Next consider Fork and Assert, then Scan and typed binding. Keep Hook as a derived pattern until examples show that a separate command makes programs clearer. Use one shared execution model for immediate pipes, scheduled pipes, and event pipes.

Use saved data and produce events

Replace a recipe’s top-level input with source to read a Store record or UTF-8 Artifact. Public reads can omit access_key. A dataset page is only one page: follow its cursor before treating a collection comparison as complete.

"source": {"dataset_id":"STORE_ID","name":"contacts","access_key":"READ_KEY"}

To save a report, append a final Store action. Revision 0 creates a record; supply the current revision for a checked update. Earlier transformation failures prevent this final write. These examples require your own dataset IDs and keys.

{"command":"store.put","options":{
  "dataset_id":"STORE_ID","access_key":"WRITE_KEY",
  "name":"report","revision":0
}}

To announce changes to a stable DNS snapshot, finish with Diff and a Notify destination. Create both datasets first. Diff stores the new snapshot and conditionally publishes in the same transaction. Its first value establishes a baseline without an alert. Future differing values emit a message; repeated values stay quiet.

{"command":"diff.post","options":{
  "dataset_id":"DIFF_ID","access_key":"DIFF_WRITE_KEY",
  "notify_id":"NOTIFY_ID","notify_key":"NOTIFY_WRITE_KEY"
}}

Free saved data is public. Private input cannot be written into public datasets. Receiver delivery happens after publication; retries may repeat a delivery event, so consumers should deduplicate its ID.

Use Schedule’s pipe mode to run a recipe on a calendar, or Events to run it when a message or queued task arrives. They use the same command steps. Dataset access · Webhook delivery · Schedule · Events.

Execution rules that matter

A consistent record envelope, explicit cardinality, typed bindings, and complete-page handling will improve composition more than adding many new names.