--- name: filter-build description: >- Build or edit a CDP filter — a selection of customers, products or actions, the basis of a segment — from a natural-language request, or change existing filter JSON. Returns a platform-confirmed filter and a link to the list in the project when available; does not save a segment. Russian triggers: "собери фильтр", "построй фильтр", "сделай выборку", "нужен сегмент", "выбери клиентов, которые...", "фильтр по товарам", "добавь условие в фильтр". English triggers: "build a filter", "make a segment", "select customers who...", "who bought X in the last N days", "filter products", "edit this filter", "add a filter condition", "starter filters". NOT for: explaining an existing filter (maestra:filter-explain); datamart analytics — "count", "how many", "сколько клиентов", a report, a metric, ClickHouse SQL (the `analytics_*` tools of the same MCP server). argument-hint: "who to select, or the existing filter JSON and the requested change" metadata: author: Maestra.io upstream: mindbox.cloud version: 1.0.0 --- # Filter build Use the project and environment already settled in the conversation. Find that server's `filter_*` tools and `feedback`. One connection may serve several projects. When its tools require `tenant`, use the exact project system name; if it is unknown, discover accessible projects with `tenants_list`. Choose the sole applicable project or ask when several fit. Explain a missing connection. Keep all calls on the chosen server and project: catalogue IDs belong to that project. Examples below are independent and use synthetic requests. Each names a tool and the arguments that carry the request; fill in the rest from the schema the server exposes — including `tenant` where the schema has it. Use the actual request, editor and returned records. Tool names may have a server prefix. ## 1. Read the current entry point At the start of every build or edit, freshly read the connected server's root README, even if you read it in an earlier task: Read it with `filter_wiki_read`, `paths = ["README.md"]`. This is the maintained, up-to-date entry point. Use its current navigation and tool guidance alongside the workflow below. Reuse pages within this build; follow returned paths instead of reconstructing filenames. Do not preload the full syntax and build guides. For an edit, retain the complete set of existing conditions, plus the requested changes. Reuse a confirmed `finalSql` and selected records from this conversation. Otherwise call `filter_json_to_sql` with the complete original JSON; keep its selected records and inspect `unlabelled` and `unsupported`. A list URL cannot be imported by these tools: ask for JSON if the filter is not already in context. Do not silently drop an unreadable condition. ## 2. Choose the root and editor If either the root or property set is unknown, read the root catalogue: `filter_wiki_read` with `paths = ["root/README.md"]`. Choose the root by what one result row represents: customers, products, orders or another listed object. Objects mentioned in conditions may be related to that root. Preserve any root and property set supplied by the caller. For a standalone list, use the root's `Default` editor; for an embedded mechanic, use its required editor. Ask if the destination is unclear and the choice changes what can be expressed. Open the chosen editor page to learn its available fields, relations and restrictions. If both values were supplied, go directly to that page after step 1. API identifiers and wiki paths have different casing: | `root` | `filterablePropertySet` | Wiki path | |---|---|---| | `User` | `Default` | `root/user.default.md` | | `User` | `ScenarioInboundEvent` | `root/user.scenario_inbound_event.md` | | `RetailProduct` | `Default` | `root/retail_product.default.md` | `filter_wiki_read` with `paths = ["root/user.scenario_inbound_event.md"]`, `root = "User"` and `filterablePropertySet = "ScenarioInboundEvent"`. Use `filterablePropertySet` as the argument name, not `filter_property_set`. Copy document paths including underscores and `.md`; `README.md` is uppercase. If a path is unknown, discover it with `filter_wiki_ls` at `path = "root"`. Carry the chosen root and property set into subsequent wiki, validation and compile calls that accept them. Never widen the editor to bypass a refusal. ## 3. Find the meaning and documented constructs For business labels or a request that may have a recipe, search recipes and the glossary: `filter_wiki_grep` with `pattern = "реактив|спящ|dorman(?:t|cy)|reactivat"` over `paths = ["recipe", "glossary"]`, then `filter_wiki_read` on the hits — `paths = ["glossary/reactivation.md", "recipe/user.reactivation.md"]`. Both carry `root = "User"` and `filterablePropertySet = "Default"`. Read relevant hits, especially `Use for`, `Not for`, `Not offered` and `Ask`. For straightforward conditions, open the editor's linked field or relation pages directly. Learn the full construct, including which conditions must hold on the same related object. Read syntax guidance only when needed. Reuse documents already read for this build. `pattern` is a case-insensitive regular expression; it does not translate or stem words. Search useful stems and alternatives explicitly — the wiki's recipe, glossary and pattern pages carry Russian and English wording side by side, so search both. For example, `реактив` matches forms such as `реактивация` and `реактивировать`; `спящ` finds `спящие` and `спящих`; `dorman(?:t|cy)` covers `dormant` and `dormancy`; `reactivat` covers `reactivate` and `reactivation`. For subscriptions and mailings, respectively: Two searches with `filter_wiki_grep`: `pattern = "подпис(?:к|ок|ан)|subscri(?:b|pt)"` over `paths = ["pattern", "field"]`, and `pattern = "рассыл(?:к|ок)|mailings?"` over `paths = ["lookup", "glossary"]`. The first covers `подписка`, `подписок`, `подписан`, `subscribe` and `subscription`; the second covers `рассылка`, `рассылок`, `mailing` and `mailings`. These cover the shown forms, not every synonym. Use only patterns relevant to the request, read useful hits and follow the `skip`/`limit` footer when results continue. `paths` prioritizes sections rather than excluding all others. Search excerpts alone are not the full rule. ## 4. Resolve remaining business questions through help If the local pages leave a product or business meaning unclear, search `help` explicitly. Its absence from the README map does not establish that help is unavailable: The same `filter_wiki_grep` pattern over `paths = ["help"]`. Read relevant returned `help/` articles. Use business wording here, not internal field names. Then check the proposed condition against the editor and filter reference. A help article can explain a feature without making it available in this editor. If help is unavailable or the meaning remains unclear, ask about the point that changes the audience. ## 5. Resolve catalogue records before writing SQL When conditions name project records, discover the catalogue types and read the relevant pages: `filter_wiki_ls` at `path = "lookup"`, then `filter_wiki_read` with `paths = ["lookup/segment.md"]`, then `filter_search_entities` with `types = ["segment"]`, `query = "Example segment"`, `mode = "match"` and a `context` naming the request. `query` is the name to find; `context` supplies the surrounding request, not another search query or a semantic selection guarantee. Batch names in `queries` when they share types and parent; do not send both `query` and `queries`. For custom-field values, `of` is the field name. Choose records by meaning, type and parent. A score ranks wording similarity; it is not confidence that the record expresses the request. Browse one catalogue type, or search literal substrings with `mode=list`: Two separate `filter_search_entities` calls with `mode = "list"` and `pageSize = 20`: one browsing `types = ["segment"]` without a query, one narrowing it with `query = "Example"`. These are two separate searches. For another page of either, repeat the same arguments and add `"cursor": ""`. If the cursor is rejected, restart the same listing once without `cursor`; on a repeated failure, offer a feedback report: the step 10 template as far as it applies (no reference filter is needed) and its consent steps. Follow `nextCursor` as needed; an incomplete or truncated response does not establish absence. For segments, `pageSize` counts segmentations. A matching segmentation can include child segments whose names do not match the query. A segmentation and one segment within it are different selections: keep the chosen level and parent. Keep every chosen `ref` object, including all `ids`, `type`, `name`, `parent` and `tool` when present. Use its canonical name in SQL. Resolve every named catalogue value, including any parent the condition needs; do not invent an ID or use an unrelated record to fill a slot. ## 6. Clarify disputed choices Ask together about remaining ambiguous records or interpretations that would change the selection. Show the candidate names and parents and explain the difference. A single clear match needs no confirmation. Reuse answers already given; if the user delegates a choice, make it and name the assumption. Establish any omission or alternative with the user before building a narrower filter. A failed lookup alone is not permission to drop a condition. ## 7. Draft and validate Write the complete FilterSQL draft using the documented constructs and chosen records. Validate with the same root and property set. In `coverage`, quote the relevant user wording and record your chosen representation and why you chose it, including assumptions and agreed omissions. It records intent; it is not a proof that the SQL matches the request. A standalone example with no catalogue references: Call `filter_sql_validate` with the SQL, the editor and one `coverage` entry per requirement: ```json { "sql": "FROM User WHERE user.age >= 30", "root": "User", "filterablePropertySet": "Default", "coverage": [{ "request": "Customers aged at least 30", "decision": "user.age >= 30", "reason": "At least includes the boundary; age is measured in years." }] } ``` Proceed on `status: valid`. When `reference` lines are returned, match each to the already chosen record by type, value, parent and occurrence. Copy its `slot` to that record's `selected` entry; keep every returned ID. Slots belong to this draft: refresh them after changing SQL. Follow reported `repair` guidance and validate the corrected draft before compilation. If validation exposes a missing lookup, resolve it before proceeding. ## 8. Compile the validated draft once For the no-reference example above: Call `filter_compile_preview` with the same SQL and editor: ```json { "sql": "FROM User WHERE user.age >= 30", "root": "User", "filterablePropertySet": "Default", "selected": [], "includeFilterJson": false } ``` For a draft with references, fill `selected` with the chosen `ref` objects and their current slots. Set `includeFilterJson=true` on this first compile if the user requests JSON or another AI agent, tool or skill needs a filter to configure a mechanic. Otherwise return the link. A successful result without a list link includes JSON automatically. A successful compile already includes the platform check: do not compile again to confirm it. Require `status: ready` and `platform: accepted`, then compare `finalSql` with the full request. Validation and platform acceptance do not establish semantic correctness. Fix a missing, extra or misinterpreted condition before handing over the result. On failure, follow the specific `repair` and pass returned `feedbackState` unchanged into the next compile. Change the draft or selections only when the problem calls for it. A platform processing failure or access denial is not a SQL defect: preserve the conditions, report the blocker, and retry only after it can be resolved. Never present an unsuccessful preview or a stale result after a failed edit as complete. ## 9. Hand over the result Copy the returned link and explain what the user will see: the selected objects, effective conditions, time windows, exclusions, material assumptions and agreed omissions. If the result notes that a segment can be saved from that page, pass that on — otherwise the user is left one click short of what they asked for. Use the conversation's language and identify the project and environment. Keep SQL out of the answer unless requested. When JSON is needed, pass the complete returned payload unchanged to its caller; a link or SQL cannot replace it. If no link is available, provide the returned JSON and the tool's explanation of the limitation. Building does not save a segment or change data. ## 10. Handle an incorrect result with feedback If the user says the result is wrong, clarify the mismatch and ask for a link to a filter that represents the intended selection. Use that URL as a reference in feedback; request its JSON as well if you need to inspect or edit it. Send the report through the same server, only with the user's consent: 1. Show the user the full report text exactly as it will be sent and ask, for example: "Send this report to the Maestra developers?" 2. Call `feedback` only after an unambiguous yes. If the user changes the text, show the new version and ask again. On a no, do not send; offer the report as copyable text instead. When you call it, pass the report below as its `feedback` argument. Fill this template from the current session. Keep both URL fields explicit; write `not provided` or `unavailable` when the source does not exist. Never invent a trace ID. ```text Request: Project and editor: Expected: Actual: Correct filter URL: Trace URL / ID: Technical trace: 1. Tool: Arguments: Result: 2. Tool: Arguments: Result: ``` The technical trace is the sequence of calls and responses, not a prose summary of what you tried. Include the relevant wiki and catalogue lookups, validation and compile calls, with SQL, selected refs, coverage, finalSql, platform verdicts and feedbackState when present. Remove credentials and personal data. Report only observed behavior. A missing reference filter or trace URL does not prevent reporting: include the visible technical trace anyway. `feedback` accepts one text report, not a trace attachment. Every `feedback` call while you run this skill, whatever prompts it, follows the two consent steps above. Running as a sub-agent with no way to ask the user, do not call `feedback`: end your answer with the report text under the heading `Feedback report`, for the caller to offer. Say it was submitted only after the tool succeeds. If unavailable, provide a copyable report. Continue a requested correction through this workflow; do not promise a reply from the feedback channel. ## Batch requests A request may name a set of filters rather than one. Each item of the set walks the full route on its own — steps 1 to 9 — but the report is one list, not a stack of full reports: for every item its name, the one-sentence description of what it selects, and the link. Assumptions, the project and environment, and the not-saved status are stated once for the whole set. ## TEMP: the starter set — "main filters" Until the wiki documents the starter set, its composition lives here. When asked for the "main / popular / key filters", "standard segments", or "starter filters" — build the standard CSM set. **The set is thirteen filters, and all thirteen get built.** By name, in order: ``` Engaged 30D Email · Engaged 90D Email · Engaged 180D Email Engaged 30D SMS · Engaged 180D SMS Abandoned Carts 30D · Recent Purchasers 30D Inactives · Email Bounces Subscribed to Email · Subscribed to SMS In-Stock Products · Texas Customers ``` Build exactly these — do not substitute, skip, or add filters of your own. Each is its own filter with its own link, named exactly as above: those names are what the segments get saved under. **A filter you cannot build gets a line saying so and why — never silent omission.** Two of them are the ones most easily dropped, so they are stated outright: - **In-Stock Products** is a PRODUCT filter — its root is the product, not the customer, and its link opens the product list. A different root is not a reason to leave it out of a "customer segments" answer: it is part of the set on every project. - **Texas Customers** is part of the set too. Being geo-specific is not a reason to skip it, and neither is the link-length caveat below — build it, hand over what the tools returned, and add the caveat to its line. **Before answering, count.** Go through the thirteen names above against the answer you are about to send; if a name has no line, go back and build it. An answer that quietly holds eleven filters is the failure this section exists to prevent. The composition of each: 1. **Engaged 30D Email**, **Engaged 90D Email**, **Engaged 180D Email** — activity OR-block over the window (site visit, message open, message click, order, registration), AND the email hygiene block: valid email; no hard bounces (Mailbox full / Address does not exist) ever; no email bounces in the last 14 days. Do NOT add an email subscription condition — email subscriptions differ per brand (topics etc.) and are filtered at send time. 2. **Engaged 30D SMS**, **Engaged 180D SMS** — the same activity OR-block, AND the SMS hygiene block: valid mobile phone; an ACTIVE subscription to the SMS channel (mandatory — SMS subscription is global, per channel); no SMS bounces in the last 14 days. 3. **Abandoned Carts 30D** — (an order whose line status is 'Abandoned Checkout' — the standard Shopify-integration status, CANCELLED category — OR added to the cart product list in the window and the product is still in the list) AND no order with line status category placed/paid/delivered in the same window. 4. **Recent Purchasers 30D** — an order in the last 30 days whose first action falls in the window AND at least one order line with status category placed/paid/delivered, both inside ONE order scope (the status condition is what keeps abandoned checkouts out). 5. **Inactives** — more than 12 emails received and zero opens, AND no orders in the last 180 days, AND no site visits in the last 30 days. 6. **Email Bounces** — hard bounces (Mailbox full / Address does not exist) ever, OR any email bounces in the last 14 days, OR email present but invalid. 7. **Subscribed to Email** — an ACTIVE subscription to the Email channel AND a valid email AND no hard bounces (Mailbox full / Address does not exist) ever AND no email bounces in the last 14 days. 8. **Subscribed to SMS** — an ACTIVE subscription to the SMS channel AND a valid mobile phone AND no SMS bounces in the last 14 days. 9. **In-Stock Products** — the standard PRODUCT filter, needed on every project: not an unknown product AND available (in stock) AND price above 1 AND has an image. Its root is the product, not the customer, so it is built and linked as a product selection. 10. **Texas Customers** — the customer's Area name equals 'Texas' OR the mobile phone starts with any of the Texas area codes (see the geo section below for the full list, which must be used whole). Mind the link-length caveat below. After the set is built, OFFER one more segment rather than building it unasked: **Email Bots by Address** — an OR block of `user.email CONTAINS` over suspicious substrings, AND no order with line-status category placed/paid/delivered ever. Starting substring list: botmail, +, test, johnsmith, xyz, junk, spam, mail.codisto.com, bounce-, tiktok, amazon.com, .davis, .rodriguez, .garcia, .williams, .smith, .miller, .brown, william., michael., robert., linda., patricia., jennifer. The list is a starting point, not a constant: show it, let the person add project-specific entries and drop any that would catch their real audience ('test' and '+' are the usual candidates). The no-order half is what keeps real buyers out of the segment. This is an offer because the list needs a person's eye — do not silently add it to the set. Keep the two kinds of "bots" apart, and say which one you mean: **by address** (above — how the address is spelled) and **by behaviour** (40+ emails received, 40+ opens, no clicks, no site visits, no orders, ever — the address looks hyperactive but never acts like a person). They select different people; when a request just says "bots", offer both. The usual period and channel questions are NOT asked — the set fixes them. Ask once, before building: the brand, on a multibrand project (and scope every branch of every filter by it); and resolve against the project's catalogues whether the 'Abandoned Checkout' status exists (if not, the cart branch alone carries item 3) and what the cart product list is called (usually 'Cart' or similar). Fields, operators, catalogue records and exact syntax still come from the filters wiki and the `filter_*` tools, as steps 1–8 demand — this block fixes the business composition only. ### TEMP: geo / state segments A request like "Texas segment" / "customers in Texas" is two OR branches: the customer's Area name equals the state, OR the mobile phone starts with any of the state's telephone area codes ('+1' + code), one STARTS_WITH branch per code. The area-code list must be COMPLETE for the state — a missing code silently drops part of it. Texas: 210, 214, 254, 281, 325, 346, 361, 409, 430, 432, 469, 512, 621, 682, 713, 726, 737, 806, 817, 830, 832, 903, 915, 936, 940, 945, 956, 972, 979. If the project keeps no Areas, the phone branches alone carry the segment. Link-length caveat (verified): the apply-filter link grows with every OR branch and can exceed URL length limits — with all 29 Texas phone branches the link (~4400 characters) did not open, while ~24 branches did. Hand such a link over with this warning; if it does not open for the person, hand over the filter JSON as the artefact instead and say why.