{
 "generatedFrom": "https://github.com/visokio/omniscope-custom-blocks",
 "kind": "blocks",
 "statement": "Items tagged visokio-agents are community examples built by Visokio's agents, to extend Omniscope's core blocks and views with popular, specialist and more unusual use cases, and to show what anyone, people or agents, can build on Omniscope. They are tested by automated tests and were run on a real Omniscope server. They were not reviewed by Omniscope's core team, so they are not on a par with the core blocks and views, which have years of manual, functional and unit tests behind them.",
 "items": [
  {
   "id": "Analytics/AB-Test",
   "name": "A/B Test",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Tests whether variants differ from the control on conversion rate (z-test) or mean (Welch's t-test), with lift, confidence intervals, p-values, verdicts and required sample size.",
   "tags": [
    "a/b test",
    "experiment",
    "significance",
    "t-test",
    "z-test",
    "conversion",
    "lift",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "variant_field",
     "title": "Variant field",
     "type": "Field",
     "description": "Field naming the variant, such as A / B or Control / New checkout."
    },
    {
     "name": "metric_field",
     "title": "Outcome field (one row per visitor)",
     "type": "Field",
     "description": "Converted (Yes / No, 1 / 0, true / false) for rates; a number for means."
    },
    {
     "name": "trials_field",
     "title": "Trials field (one row per variant)",
     "type": "Field",
     "description": "For example Visitors."
    },
    {
     "name": "successes_field",
     "title": "Successes field (one row per variant)",
     "type": "Field",
     "description": "For example Conversions."
    },
    {
     "name": "segment_fields",
     "title": "Test separately for each (optional)",
     "type": "Fields",
     "description": "For example Device or Country."
    }
   ],
   "options": [
    {
     "name": "layout",
     "title": "Data layout",
     "type": "Choice",
     "description": "Raw rows with one outcome each, or a summary with trials and successes per variant."
    },
    {
     "name": "metric_type",
     "title": "Compare",
     "type": "Choice",
     "description": "A rate (converted or not) is tested with a z-test; a mean with Welch's t-test."
    },
    {
     "name": "control",
     "title": "Control variant",
     "type": "Text",
     "description": "Value of the variant field for the control. Leave empty to use the first in alphabetical order."
    },
    {
     "name": "confidence",
     "title": "Confidence level",
     "type": "Number",
     "description": "0.95 means a 5% chance of calling a difference that is really chance."
    },
    {
     "name": "power",
     "title": "Power for sample size",
     "type": "Number",
     "description": "Chance of detecting a real effect of the observed size; 0.8 is the usual choice."
    },
    {
     "name": "bonferroni",
     "title": "Bonferroni correction",
     "type": "Yes / no",
     "description": "Divide the significance threshold by the number of comparisons, to protect against false positives when testing many variants or segments."
    }
   ],
   "outputs": [
    {
     "id": "results",
     "label": "Results",
     "description": "One row per variant (and segment) compared with the control: difference, lift, confidence interval, p-value, significance, sample size needed, verdict."
    },
    {
     "id": "variants",
     "label": "Variants",
     "description": "One row per variant (and segment): sample size, rate or mean, standard deviation, confidence interval."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **One Way ANOVA** block does not test conversion rates, give lift with confidence intervals or compare each variant with a control; this block does, from per-variant totals, with a plain-language verdict.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "abtest",
     "name": "A/B Test",
     "page": "https://visokio.github.io/omniscope-custom-views/abtest/index.html",
     "note": "Runs the same comparisons with the control (z-test for rates, Welch for means, optional Bonferroni) inside a report, so the results follow its filters. It needs one row per visitor; use this block for per-variant totals, for tests in several segments at once, or for a results table you can keep."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "One Way ANOVA",
     "page": null,
     "note": "Compares means across groups."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Sample-Size-Calculator",
     "name": "Sample Size Calculator",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Sample-Size-Calculator/index.html",
     "note": "Works out how many visitors a test needs before it starts."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/AB-Test/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/AB-Test",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/AB-Test/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/AB-Test/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/AB-Test/sample-data/checkout-experiment-summary.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/AB-Test/sample-data/checkout-experiment.csv"
    ]
   }
  },
  {
   "id": "Analytics/ABC-Classification",
   "name": "ABC Classification",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Supply Chain",
   "description": "Classifies items into A, B and C by cumulative share of value (Pareto), optionally within groups, plus XYZ demand-variability classes for ABC-XYZ inventory analysis.",
   "tags": [
    "abc analysis",
    "pareto",
    "xyz",
    "inventory",
    "classification",
    "80/20",
    "supply chain",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "Product, SKU, customer or supplier."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "Value to rank by, such as sales value or margin. Summed per item."
    },
    {
     "name": "group_fields",
     "title": "Classify within (optional)",
     "type": "Fields",
     "description": "For example Warehouse or Category."
    },
    {
     "name": "date_field",
     "title": "Date field (enables XYZ)",
     "type": "Field",
     "description": "Date of each sales line; demand is summed per period."
    },
    {
     "name": "xyz_field",
     "title": "Demand field for XYZ",
     "type": "Field",
     "description": "Quantity to measure variability on, such as Units. Leave empty to use the value field."
    }
   ],
   "options": [
    {
     "name": "a_share",
     "title": "A class up to",
     "type": "Number",
     "description": "Cumulative share of value covered by class A."
    },
    {
     "name": "b_share",
     "title": "B class up to",
     "type": "Number",
     "description": "Cumulative share covered by classes A and B together."
    },
    {
     "name": "period",
     "title": "Period",
     "type": "Choice",
     "description": "Period that demand is summed over before its variability (CV) is measured."
    },
    {
     "name": "x_cv",
     "title": "X class up to CV",
     "type": "Number"
    },
    {
     "name": "y_cv",
     "title": "Y class up to CV",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "items",
     "label": "Items",
     "description": "Per item: total value, value share, cumulative share, rank, ABC class; with a date field also periods, average per period, coefficient of variation, XYZ class and ABC-XYZ."
    },
    {
     "id": "classes",
     "label": "Classes",
     "description": "Per (group and) class: items, value, share of items, share of value, suggested policy."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks an ABC split takes a sort, a running total and formulas, and is awkward per group or with XYZ demand variability. This block classifies every item in one step.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "abcxyz",
     "name": "ABC-XYZ Analysis",
     "page": "https://visokio.github.io/omniscope-custom-views/abcxyz/index.html",
     "note": "Classifies the same way (ABC by share of value, XYZ by demand variability, same default thresholds) and shows the nine classes as a matrix in a report, recalculated for any filter. Use this block when you need each item's class as a field, for joins, exports or stock rules."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Concentration-Measures",
     "name": "Concentration Measures",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Concentration-Measures/index.html",
     "note": "HHI, Gini and top-N shares summarising how concentrated the total is."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Safety-Stock",
     "name": "Safety Stock and Reorder Point",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Safety-Stock/index.html",
     "note": "Stock levels per item, often set differently for A, B and C items."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Tile",
     "name": "Tile",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Tile/index.html",
     "note": "Equal-sum (Pareto) bands and other tiles of a numeric field."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/ABC-Classification/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/ABC-Classification",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/ABC-Classification/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/ABC-Classification/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/ABC-Classification/sample-data/sku-monthly-sales.csv"
    ]
   }
  },
  {
   "id": "Analytics/Basket-Affinity",
   "name": "Basket Affinity Matrix",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Retail",
   "description": "Measures how often every pair of items is bought together (support, confidence, lift, Jaccard) from basket data, with each item's strongest partners.",
   "tags": [
    "market basket",
    "affinity",
    "lift",
    "cross-sell",
    "co-occurrence",
    "retail",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "basket_field",
     "title": "Basket / order ID",
     "type": "Field",
     "description": "Identifies a basket: rows with the same value were bought together."
    },
    {
     "name": "item_field",
     "title": "Item / product",
     "type": "Field",
     "description": "The item in each row, such as a product name or SKU."
    }
   ],
   "options": [
    {
     "name": "max_items",
     "title": "Most frequent items to include",
     "type": "Whole number",
     "description": "Only this many of the most often bought items are paired, which keeps the output readable."
    },
    {
     "name": "min_item_baskets",
     "title": "Minimum baskets per item",
     "type": "Whole number"
    },
    {
     "name": "min_pair_baskets",
     "title": "Minimum baskets per pair",
     "type": "Whole number",
     "description": "Pairs seen together fewer times are left out."
    },
    {
     "name": "top_partners",
     "title": "Partners listed per item",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "pairs",
     "label": "Pairs",
     "description": "One row per item pair: baskets with both, support, confidence both ways, lift, Jaccard, association."
    },
    {
     "id": "items",
     "label": "Items",
     "description": "Per item: baskets, support and strongest partners by lift."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Basket Analysis** block finds strong association rules; this block keeps every product pair, weak pairs included, for heat maps and range reviews, so the two complement each other.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Basket Analysis",
     "page": null,
     "note": "Association rules for items bought together. This block outputs the full pairwise matrix (support, confidence, lift, Jaccard) for heat maps."
    },
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "marketbasket",
     "name": "Market Basket",
     "page": "https://visokio.github.io/omniscope-custom-views/marketbasket/index.html",
     "note": "Measures lift, confidence and support for item pairs from the same order lines and draws them as a clustered matrix with ranked rules, in a report. Use this block to keep every pair as data, with Jaccard and each item's strongest partners."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Product-Recommendations",
     "name": "Product Recommendations",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Product-Recommendations/index.html",
     "note": "Turns co-purchase similarity into a ranked list of products per customer."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Basket-Affinity/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Basket-Affinity",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Basket-Affinity/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Basket-Affinity/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Basket-Affinity/sample-data/grocery-baskets.csv"
    ]
   }
  },
  {
   "id": "Analytics/Benford-Test",
   "name": "Benford's Law Test",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Audit",
   "description": "Tests whether amounts follow Benford's law (first, first-two or second digit), overall or per group, with chi-square, MAD conformity and flagged records for follow-up.",
   "tags": [
    "benford",
    "fraud",
    "audit",
    "forensic",
    "anomaly",
    "digits",
    "data quality",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "value_field",
     "title": "Amount field",
     "type": "Field",
     "description": "The amounts to test, such as invoice or expense values."
    },
    {
     "name": "group_fields",
     "title": "Test separately for each (optional)",
     "type": "Fields",
     "description": "For example Department, Supplier or Employee."
    }
   ],
   "options": [
    {
     "name": "test",
     "title": "Test",
     "type": "Choice",
     "description": "Which digits to compare with Benford's law. First two digits is the finer test usually used in audit."
    },
    {
     "name": "min_value",
     "title": "Ignore amounts below",
     "type": "Number",
     "description": "Small amounts distort first-digit tests; 10 is the usual floor."
    },
    {
     "name": "min_records",
     "title": "Minimum values per group",
     "type": "Whole number",
     "description": "Groups with fewer values get no conformity verdict."
    },
    {
     "name": "p_threshold",
     "title": "Significance for nonconformity",
     "type": "Number",
     "description": "A group is only called nonconforming when its MAD is in the nonconformity band and the chi-square p-value is below this."
    },
    {
     "name": "z_threshold",
     "title": "Z threshold for flagging digits",
     "type": "Number",
     "description": "1.96 is about 95% confidence; 2.57 about 99%."
    }
   ],
   "outputs": [
    {
     "id": "digits",
     "label": "Digits",
     "description": "Per group and digit: count, observed and expected share, difference, z-statistic, flag."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per group: values tested, chi-square, p-value, MAD, conformity verdict, over-represented digits."
    },
    {
     "id": "flagged",
     "label": "Flagged",
     "description": "Input records whose tested digits are over-represented in a nonconforming group."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "benford",
     "name": "Benford Analysis",
     "page": "https://visokio.github.io/omniscope-custom-views/benford/index.html",
     "note": "Runs the same digit tests with MAD conformity in a report, ranks groups by deviation and selects the records behind a digit. Use this block to keep the conformity summary and the flagged records for follow-up."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Sequence-Gap-Finder",
     "name": "Sequence Gap Finder",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Sequence-Gap-Finder/index.html",
     "note": "Another audit screen: missing, duplicate and out-of-order document numbers."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Benford-Test/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Benford-Test",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Benford-Test/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Benford-Test/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Benford-Test/sample-data/expense-claims.csv"
    ]
   }
  },
  {
   "id": "Analytics/Case-Bottlenecks",
   "name": "Case Duration and Bottlenecks",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Process",
   "description": "From an event log: case durations and rework, the slowest and costliest handovers between activities, and activity statistics.",
   "tags": [
    "process mining",
    "bottlenecks",
    "cycle time",
    "event log",
    "rework",
    "case management",
    "operations",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "case_field",
     "title": "Case field",
     "type": "Field",
     "description": "Identifies a case, such as a claim, ticket or order number."
    },
    {
     "name": "activity_field",
     "title": "Activity field",
     "type": "Field",
     "description": "The name of the step recorded in each event."
    },
    {
     "name": "time_field",
     "title": "Timestamp field",
     "type": "Field",
     "description": "When the event happened; orders the events within each case."
    }
   ],
   "options": [
    {
     "name": "unit",
     "title": "Durations in",
     "type": "Choice",
     "description": "Unit for durations and waiting times."
    },
    {
     "name": "max_variant_steps",
     "title": "Steps shown in a route",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "cases",
     "label": "Cases",
     "description": "Per case: start, end, duration, steps, rework, first and last activity, route, variant rank, slowest 10%."
    },
    {
     "id": "handovers",
     "label": "Handovers",
     "description": "Per pair of consecutive activities: count, cases, median / average / 90th percentile / total time, share of waiting, ranks."
    },
    {
     "id": "activities",
     "label": "Activities",
     "description": "Per activity: occurrences, cases, rework, median time to next step, times last step."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Network Analysis** block analyses links between nodes, not timed event logs. This block measures case durations, handover times and rework from an event log as tables.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Path-Analysis",
     "name": "Path Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Path-Analysis/index.html",
     "note": "The most common routes in more detail, with transitions for Sankey charts."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "processmap",
     "name": "Process Map",
     "page": "https://visokio.github.io/omniscope-custom-views/processmap/index.html",
     "note": "Draws the process map from the same event log, with rework loops and variants, and selects cases."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Case-Bottlenecks/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Case-Bottlenecks",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Case-Bottlenecks/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Case-Bottlenecks/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Case-Bottlenecks/sample-data/claims-events.csv"
    ]
   }
  },
  {
   "id": "Analytics/Churn-Survival",
   "name": "Churn Survival Curves",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Kaplan-Meier retention curves with censoring, confidence bands, median lifetime, milestone retention and log-rank tests between groups.",
   "tags": [
    "survival analysis",
    "Kaplan-Meier",
    "churn",
    "retention",
    "log-rank",
    "censoring",
    "customer lifetime",
    "subscriptions",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Start date",
     "type": "Field",
     "description": "Date each customer or subscription started."
    },
    {
     "name": "end_field",
     "title": "End date",
     "type": "Field",
     "description": "Empty = still active."
    },
    {
     "name": "duration_field",
     "title": "Or a duration field",
     "type": "Field",
     "description": "Instead of dates, in the unit below."
    },
    {
     "name": "event_field",
     "title": "Event field (optional)",
     "type": "Field",
     "description": "Yes / true / 1 = ended (churned). Other values are censored."
    },
    {
     "name": "group_field",
     "title": "Compare groups (optional)",
     "type": "Field",
     "description": "For example Plan or Channel."
    }
   ],
   "options": [
    {
     "name": "as_of",
     "title": "As of date (optional)",
     "type": "Text",
     "description": "Default: the latest date in the data."
    },
    {
     "name": "unit",
     "title": "Time unit",
     "type": "Choice",
     "description": "Unit for lifetimes, the curve and the milestones."
    },
    {
     "name": "milestones",
     "title": "Retention milestones",
     "type": "Text",
     "description": "In the time unit, for example: 3, 6, 12, 24"
    },
    {
     "name": "confidence",
     "title": "Confidence level",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "curve",
     "label": "Curve",
     "description": "Per group and time: at risk, ended, censored, retention, confidence band, churned share."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per group: customers, ended, censored, median lifetime and CI, retention at milestones, restricted mean, log-rank vs rest."
    },
    {
     "id": "tests",
     "label": "Tests",
     "description": "Log-rank test across all groups."
    }
   ],
   "libraries": [],
   "relatedSummary": "A Python alternative to the community R **Survival Analysis** block, aimed at churn: it starts from start and end dates. Choose the R block if you already work in R.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Survival",
     "name": "Survival Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Survival/index.html",
     "note": "Kaplan-Meier (or Fleming-Harrington) survival curves for censored data, using the R survival package. This block adds per-group median lifetime, milestone retention and log-rank tests, and needs no extra packages."
    },
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "survivalcurve",
     "name": "Survival Curve",
     "page": "https://visokio.github.io/omniscope-custom-views/survivalcurve/index.html",
     "note": "Uses the same Kaplan-Meier method (Greenwood bands, log-rank test) and draws the curves interactively in a report. Use this block for retention at milestones, restricted mean lifetime and the curves as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Cohort-Retention",
     "name": "Cohort Retention",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Cohort-Retention/index.html",
     "note": "Retention per acquisition cohort and period, as a cohort matrix."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Churn-Survival/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Churn-Survival",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Churn-Survival/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Churn-Survival/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Churn-Survival/sample-data/subscriptions.csv"
    ]
   }
  },
  {
   "id": "Analytics/Clustering/DBScan",
   "name": "DBScan",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Clustering",
   "description": "Groups records into clusters by density (DBSCAN) on numeric fields and marks isolated records as noise. Outputs the input records with a Cluster field added: 1, 2, 3 and so on for clusters, 0 for noise.",
   "tags": [
    "dbscan",
    "clustering",
    "density",
    "outliers",
    "noise",
    "segmentation"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fieldsToUse",
     "title": "Fields to cluster on",
     "type": "Fields",
     "description": "The numeric fields to cluster on. Used when \"Use all numeric fields\" is unticked."
    }
   ],
   "options": [
    {
     "name": "useAllNumericFields",
     "title": "Use all numeric fields",
     "type": "Yes / no",
     "description": "Cluster on every Integer and Decimal field in the input. Untick to choose the fields in \"Fields to cluster on\"."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Clustered",
     "description": "The input records with a Cluster field: 1, 2, 3... for clusters, 0 for noise, empty where a chosen field is missing."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Clustering** block is the first choice. **DBScan** needs no cluster count and standardises the fields itself, and the records it leaves as noise are a ready-made list of outliers.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Clustering",
     "page": null,
     "note": "Groups rows into a chosen or automatic number of clusters, optionally per partition. This block finds clusters of any shape by density and marks isolated records as noise; use the core block for large inputs."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/KMeans",
     "name": "KMeans",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/KMeans/index.html",
     "note": "Puts every record into one of a chosen number of clusters. Use DBScan when you don't know how many clusters to expect or want the outliers left out."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/GMM",
     "name": "Gaussian Mixture Model",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/GMM/index.html",
     "note": "Fits overlapping oval clusters and gives each record a membership probability. DBScan suits irregular shapes and leaves noise unclustered."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/DBScan/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Clustering/DBScan",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/DBScan/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Clustering/DBScan/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/DBScan/sample-data/customer-spend.csv"
    ]
   }
  },
  {
   "id": "Analytics/Clustering/GMM",
   "name": "Gaussian Mixture Model",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Clustering",
   "description": "Clusters records with a Gaussian mixture model on numeric fields, choosing the number of clusters itself, and gives each record its cluster and the probability that it belongs there. A second input can be assigned to the same clusters.",
   "tags": [
    "gmm",
    "gaussian",
    "mixture",
    "model",
    "clustering",
    "mclust",
    "segmentation",
    "unsupervised"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "fieldsToUse",
     "title": "Fields to cluster on",
     "type": "Fields",
     "description": "The numeric fields to cluster on. Used when \"Use all numeric fields\" is unticked. A second input must have fields with the same names."
    }
   ],
   "options": [
    {
     "name": "useAllNumericFields",
     "title": "Use all numeric fields",
     "type": "Yes / no",
     "description": "Cluster on every Integer and Decimal field in the first input. Untick to choose the fields in \"Fields to cluster on\"."
    },
    {
     "name": "maxClusters",
     "title": "Maximum number of clusters",
     "type": "Whole number",
     "description": "The block fits models with 1 up to this many clusters and keeps the one with the best BIC (a measure of fit that penalises extra clusters). Larger values take longer."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Clustered",
     "description": "The first input with Cluster (1, 2, 3...) and Cluster probability (0 to 1); both empty where a chosen field is missing."
    },
    {
     "id": "Output Data 2",
     "label": "Assigned",
     "description": "The second input with the cluster each record most probably belongs to, and that probability. Empty unless a second input is connected."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Clustering** block is the first choice, especially for large inputs or without R, and works best on compact, round groups. This block fits a Gaussian mixture for clusters that differ in size, shape and tilt, so borderline records show up.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Clustering",
     "page": null,
     "note": "Groups rows into clusters, optionally per partition. This block fits a Gaussian mixture (R mclust), gives each record the probability it belongs to its cluster, and can assign a second input to the same clusters."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/KMeans",
     "name": "KMeans",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/KMeans/index.html",
     "note": "k-means with a number of clusters you choose. Quicker to explain; use this block when clusters differ in size or shape or overlap."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/DBScan",
     "name": "DBScan",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/DBScan/index.html",
     "note": "Density clusters of any shape, with isolated records marked as noise. This block assigns every record and gives a probability."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/GMM/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Clustering/GMM",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/GMM/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Clustering/GMM/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/GMM/sample-data/new-readings.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/GMM/sample-data/pump-readings.csv"
    ]
   }
  },
  {
   "id": "Analytics/Clustering/KMeans",
   "name": "KMeans",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Clustering",
   "description": "Groups records into a chosen number of clusters with k-means on numeric fields, standardised first. Outputs the input records with a Cluster field added.",
   "tags": [
    "kmeans",
    "k-means",
    "clustering",
    "segmentation",
    "unsupervised"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "fieldsToUse",
     "title": "Fields to cluster on",
     "type": "Fields",
     "description": "The numeric fields to cluster on. Used when \"Use all numeric fields\" is unticked."
    }
   ],
   "options": [
    {
     "name": "useAllNumericFields",
     "title": "Use all numeric fields",
     "type": "Yes / no",
     "description": "Cluster on every Integer and Decimal field in the input. Untick to choose the fields in \"Fields to cluster on\"."
    },
    {
     "name": "numClusters",
     "title": "Number of clusters",
     "type": "Whole number",
     "description": "How many clusters to make (the k in k-means). Every record with values in the chosen fields goes into one of them."
    },
    {
     "name": "standardise",
     "title": "Standardise fields",
     "type": "Yes / no",
     "description": "Rescale each chosen field to mean 0 and standard deviation 1 before clustering, so a field measured in thousands does not outweigh one measured in single figures. Untick when the fields share a unit and scale, such as latitude and longitude."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Clustered",
     "description": "The input records with a Cluster field (1, 2, 3...); empty where a chosen field is missing."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Clustering** block is the first choice. This block is a plain R k-means that is easy to explain and extend, and standardises the fields itself.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Clustering",
     "page": null,
     "note": "Can choose the number of clusters itself and cluster each partition separately. This block runs R's k-means with a fixed seed, so the same input always gives the same clusters."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/DBScan",
     "name": "DBScan",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/DBScan/index.html",
     "note": "Finds clusters by density, chooses their number itself and marks outliers as noise. Use KMeans when you want a set number of compact groups covering every record."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Clustering/GMM",
     "name": "Gaussian Mixture Model",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/GMM/index.html",
     "note": "Chooses the number of clusters by BIC and allows long, tilted or overlapping clusters, with a probability per record. KMeans is simpler and assumes round clusters of similar size."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "scatter3d",
     "name": "3D Scatter",
     "page": "https://visokio.github.io/omniscope-custom-views/scatter3d/index.html",
     "note": "Shows the clusters in 3D on three numeric fields, coloured by Cluster."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Clustering/KMeans/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Clustering/KMeans",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/KMeans/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Clustering/KMeans/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Clustering/KMeans/sample-data/store-metrics.csv"
    ]
   }
  },
  {
   "id": "Analytics/Cohort-Retention",
   "name": "Cohort Retention",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "Builds acquisition cohorts from activity or order data and measures retention and revenue per cohort over time, with an average retention curve.",
   "tags": [
    "cohort",
    "retention",
    "churn",
    "lifetime value",
    "subscription",
    "saas",
    "e-commerce",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "customer_field",
     "title": "Customer field",
     "type": "Field",
     "description": "Identifies a customer; the first date seen sets the cohort."
    },
    {
     "name": "date_field",
     "title": "Activity or order date",
     "type": "Field",
     "description": "Date of each activity or order."
    },
    {
     "name": "revenue_field",
     "title": "Revenue field (optional)",
     "type": "Field",
     "description": "Adds revenue, revenue per cohort customer and cumulative revenue per customer."
    }
   ],
   "options": [
    {
     "name": "period",
     "title": "Cohort period",
     "type": "Choice",
     "description": "Length of a cohort, and of each step along the retention curve."
    },
    {
     "name": "max_periods",
     "title": "Periods to follow",
     "type": "Whole number",
     "description": "How many periods after acquisition to track."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April."
    }
   ],
   "outputs": [
    {
     "id": "matrix",
     "label": "Matrix",
     "description": "Cohort x period number: active customers, retention, revenue, cumulative revenue per customer."
    },
    {
     "id": "curve",
     "label": "Curve",
     "description": "Average retention by period number across cohorts (weighted and mean)."
    },
    {
     "id": "customers",
     "label": "Customers",
     "description": "Per customer: cohort, last active period, periods active, active in the latest period."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks, cohort retention takes several Aggregate and Join blocks and a pivot, and the cohort logic is easy to get wrong. This block builds the cohort matrix in one step.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "cohortretention",
     "name": "Cohort Retention",
     "page": "https://visokio.github.io/omniscope-custom-views/cohortretention/index.html",
     "note": "Builds the same cohorts from raw activity and shows them as a triangle heatmap with retention curves, recalculated when the report is filtered. Use this block when the cohort matrix or each customer's cohort is needed as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Churn-Survival",
     "name": "Churn Survival Curves",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Churn-Survival/index.html",
     "note": "Kaplan-Meier retention from start and end dates, with median lifetime and tests between groups."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Customer-Lifetime-Value",
     "name": "Customer Lifetime Value",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Customer-Lifetime-Value/index.html",
     "note": "Predicts each customer's future spend and probability of still being active."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Cohort-Retention/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Cohort-Retention",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Cohort-Retention/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Cohort-Retention/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Cohort-Retention/sample-data/subscription-activity.csv"
    ]
   }
  },
  {
   "id": "Analytics/Concentration-Measures",
   "name": "Concentration Measures",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "HHI, Gini, top-N and top-20% shares and Lorenz curves for customer, supplier or market concentration, per group.",
   "tags": [
    "concentration",
    "HHI",
    "Herfindahl",
    "Gini",
    "Lorenz curve",
    "market share",
    "supplier risk",
    "customer concentration",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "entity_field",
     "title": "Entity field",
     "type": "Field",
     "description": "For example Customer, Supplier or Company."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "For example Revenue or Spend."
    },
    {
     "name": "group_fields",
     "title": "Per group (optional)",
     "type": "Fields",
     "description": "For example Year or Market."
    }
   ],
   "options": [
    {
     "name": "hhi_moderate",
     "title": "HHI: moderately concentrated from",
     "type": "Number",
     "description": "HHI at or above this is labelled moderately concentrated."
    },
    {
     "name": "hhi_high",
     "title": "HHI: highly concentrated above",
     "type": "Number",
     "description": "HHI above this is labelled highly concentrated."
    }
   ],
   "outputs": [
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per group: entities, total, HHI, normalised HHI, equivalent entities, Gini, top shares, entities for 50% / 80%, band."
    },
    {
     "id": "lorenz",
     "label": "Lorenz",
     "description": "Per group: 101 points of cumulative share of entities vs cumulative share of total."
    },
    {
     "id": "entities",
     "label": "Entities",
     "description": "Per group and entity: value, share, rank, cumulative share, in top 20%."
    }
   ],
   "libraries": [],
   "relatedSummary": "HHI, Gini and Lorenz curves are possible per group with sorts, running totals and formulas; this block calculates them in one step.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "concentration",
     "name": "Concentration",
     "page": "https://visokio.github.io/omniscope-custom-views/concentration/index.html",
     "note": "Draws Lorenz curves with Gini, top shares and HHI from the same items and measure, and selects the largest items with one click. Use this block for the measures as a table per group, with HHI bands and CR4."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/ABC-Classification",
     "name": "ABC Classification",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/ABC-Classification/index.html",
     "note": "Classifies each item A, B or C by its cumulative share of value (Pareto)."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Top-N-With-Other",
     "name": "Top N With Other",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Top-N-With-Other/index.html",
     "note": "Keeps the largest items per group and rolls the rest into Other, with shares."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Concentration-Measures/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Concentration-Measures",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Concentration-Measures/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Concentration-Measures/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Concentration-Measures/sample-data/supplier-spend.csv"
    ]
   }
  },
  {
   "id": "Analytics/Contingency-Table",
   "name": "Contingency Table and Chi-square",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Cross-tabulates two categorical fields with shares, expected counts and residuals, and tests their association with chi-square and Cramer's V.",
   "tags": [
    "contingency table",
    "crosstab",
    "chi-square",
    "cramer's v",
    "association",
    "statistics",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "row_field",
     "title": "Row field",
     "type": "Field",
     "description": "Category shown down the rows, such as Channel."
    },
    {
     "name": "col_field",
     "title": "Column field",
     "type": "Field",
     "description": "Category shown across the columns, such as Outcome."
    },
    {
     "name": "weight_field",
     "title": "Count / weight field (optional)",
     "type": "Field",
     "description": "Use when each row is already a count, such as an aggregated table."
    },
    {
     "name": "segment_fields",
     "title": "Test separately for each (optional)",
     "type": "Fields"
    }
   ],
   "options": [
    {
     "name": "alpha",
     "title": "Significance level",
     "type": "Number",
     "description": "A p-value below this calls the association significant."
    },
    {
     "name": "yates",
     "title": "Yates correction for 2 x 2 tables",
     "type": "Yes / no"
    },
    {
     "name": "include_blank",
     "title": "Include blank values as a category",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "cells",
     "label": "Cells",
     "description": "One row per row / column combination: count, shares, expected count, adjusted residual, chi-square contribution."
    },
    {
     "id": "test",
     "label": "Chi-square",
     "description": "Chi-square, degrees of freedom, p-value, Cramer's V, sample size, smallest expected count, verdict."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Frequency Table** block gives the counts; this block adds shares, expected counts and the standardised residuals that show which cells drive the difference.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "indexheatmap",
     "name": "Index Heatmap",
     "page": "https://visokio.github.io/omniscope-custom-views/indexheatmap/index.html",
     "note": "Shows the same cross-tab as index scores against independence in a report, marking cells by the same adjusted residual. Use this block for the chi-square test, Cramer's V and the cells as data."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Frequency Table",
     "page": null,
     "note": "Row counts for each combination of the selected fields."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Contingency-Table/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Contingency-Table",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Contingency-Table/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Contingency-Table/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Contingency-Table/sample-data/support-contacts.csv"
    ]
   }
  },
  {
   "id": "Analytics/Correlation-Matrix",
   "name": "Correlation Matrix",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Pearson and Spearman correlations with p-values for all pairs of numeric fields or against a target, per group, ranked, with a heat-map matrix.",
   "tags": [
    "correlation",
    "Pearson",
    "Spearman",
    "p-value",
    "heat map",
    "statistics",
    "exploratory analysis",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Numeric fields",
     "type": "Fields",
     "description": "Leave empty to use every numeric field."
    },
    {
     "name": "target_field",
     "title": "Target (optional)",
     "type": "Field",
     "description": "Correlate every field with this one only."
    },
    {
     "name": "group_field",
     "title": "Per group (optional)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "method",
     "title": "Rank and label by",
     "type": "Choice"
    },
    {
     "name": "significance",
     "title": "Significance level",
     "type": "Number"
    },
    {
     "name": "min_pairs",
     "title": "Minimum rows per pair",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "pairs",
     "label": "Pairs",
     "description": "Per pair: rows, Pearson and Spearman with p-values, strength, direction, significant, rank."
    },
    {
     "id": "matrix",
     "label": "Long matrix",
     "description": "Row field, column field, correlation, p-value, rows: for heat maps."
    },
    {
     "id": "wide",
     "label": "Wide matrix",
     "description": "One row per field, one column per field."
    }
   ],
   "libraries": [],
   "relatedSummary": "If a covariance or correlation matrix is all you need, use the core **Covariance Correlation** block. This block adds ranked pairs and p-values, per group.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Covariance Correlation",
     "page": null,
     "note": "Builds a covariance or correlation matrix from numeric fields. This block reports Pearson and Spearman correlations with p-values, per group or against a target."
    },
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "correlationmatrix",
     "name": "Correlation Matrix",
     "page": "https://visokio.github.io/omniscope-custom-views/correlationmatrix/index.html",
     "note": "Calculates Pearson or Spearman correlations in the browser, shows them as a heatmap and plots any pair on click. It gives no p-values; use this block for significance, correlations per group or every field against one target."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Correlation-Matrix/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Correlation-Matrix",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Correlation-Matrix/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Correlation-Matrix/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Correlation-Matrix/sample-data/store-performance.csv"
    ]
   }
  },
  {
   "id": "Analytics/Cost-Allocation",
   "name": "Cost Allocation",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Apportions cost pools to departments, products or customers by drivers, with penny-exact largest-remainder rounding and a list of unallocated pools.",
   "tags": [
    "cost allocation",
    "apportionment",
    "overheads",
    "activity-based costing",
    "recharges",
    "management accounts",
    "rounding",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "pool_field",
     "title": "Pool name",
     "type": "Field",
     "description": "Names each cost pool, such as IT or Facilities."
    },
    {
     "name": "amount_field",
     "title": "Amount",
     "type": "Field",
     "description": "The cost of each pool to be shared out."
    },
    {
     "name": "pool_driver_field",
     "title": "Driver for the pool (optional)",
     "type": "Field",
     "description": "Which driver shares out this pool, such as Headcount or Floor Area."
    },
    {
     "name": "receiver_field",
     "title": "Receiver",
     "type": "Field",
     "description": "The department, product or customer receiving a share."
    },
    {
     "name": "driver_field",
     "title": "Driver name (long form)",
     "type": "Field",
     "description": "Leave empty for wide form (one column per driver)."
    },
    {
     "name": "value_field",
     "title": "Driver value (long form)",
     "type": "Field",
     "description": "Size of the driver for each receiver, when drivers come one per row."
    }
   ],
   "options": [
    {
     "name": "default_driver",
     "title": "Default driver",
     "type": "Text",
     "description": "For pools without one."
    },
    {
     "name": "decimals",
     "title": "Decimals",
     "type": "Whole number",
     "description": "2 = to the penny."
    }
   ],
   "outputs": [
    {
     "id": "allocations",
     "label": "Allocations",
     "description": "Per pool and receiver: driver, driver value, share, allocated amount."
    },
    {
     "id": "receivers",
     "label": "Receivers",
     "description": "Per receiver: total allocated, from each pool, share of all costs."
    },
    {
     "id": "unallocated",
     "label": "Unallocated",
     "description": "Pools that could not be allocated, with the reason."
    }
   ],
   "libraries": [],
   "relatedSummary": "Allocating a cost pool by drivers is possible with core **Join** blocks and formulas, but the rounded shares rarely add back to the pool exactly. This block allocates by drivers with penny-exact rounding.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Cost-Allocation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Cost-Allocation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Cost-Allocation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Cost-Allocation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Cost-Allocation/sample-data/cost-pools.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Cost-Allocation/sample-data/drivers.csv"
    ]
   }
  },
  {
   "id": "Analytics/Customer-Lifetime-Value",
   "name": "Customer Lifetime Value",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "Historical and predictive customer lifetime value from orders (BG/NBD and Gamma-Gamma), with probability active, discounting and a holdout check.",
   "tags": [
    "CLV",
    "customer lifetime value",
    "BG/NBD",
    "Gamma-Gamma",
    "churn",
    "retention",
    "customer analytics",
    "RFM",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "customer_field",
     "title": "Customer field",
     "type": "Field",
     "description": "Identifies a customer."
    },
    {
     "name": "date_field",
     "title": "Order date field",
     "type": "Field",
     "description": "Date of each order."
    },
    {
     "name": "value_field",
     "title": "Order value field",
     "type": "Field",
     "description": "Revenue or profit per order line."
    },
    {
     "name": "group_fields",
     "title": "Summarise by (optional)",
     "type": "Fields",
     "description": "For example acquisition channel or region (taken from each customer's first order)."
    }
   ],
   "options": [
    {
     "name": "as_of",
     "title": "As of date (optional)",
     "type": "Text",
     "description": "Default: the latest order date."
    },
    {
     "name": "horizon_months",
     "title": "Horizon (months)",
     "type": "Whole number",
     "description": "How far ahead the predicted value runs."
    },
    {
     "name": "discount_rate",
     "title": "Annual discount rate",
     "type": "Number",
     "description": "For example 0.1 for 10%."
    },
    {
     "name": "margin",
     "title": "Margin",
     "type": "Number",
     "description": "Share of order value that is profit; 1 = predict revenue."
    },
    {
     "name": "holdout_months",
     "title": "Holdout check (months)",
     "type": "Whole number",
     "description": "0 = no check."
    }
   ],
   "outputs": [
    {
     "id": "customers",
     "label": "Customers",
     "description": "Per customer: history, probability active, status, expected purchases and order value, predicted CLV, CLV tier."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Overall and per group: customers, active share, revenue to date, predicted CLV total, average and concentration."
    },
    {
     "id": "model",
     "label": "Model",
     "description": "Fitted BG/NBD and Gamma-Gamma parameters, checks and holdout accuracy."
    },
    {
     "id": "validation",
     "label": "Holdout",
     "description": "Actual vs predicted purchases in the holdout period by number of earlier repeat purchases."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/RFM-Segmentation",
     "name": "RFM Segmentation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/RFM-Segmentation/index.html",
     "note": "Scores customers on recency, frequency and spend and names segments, without a prediction."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Cohort-Retention",
     "name": "Cohort Retention",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Cohort-Retention/index.html",
     "note": "Retention and cumulative revenue per acquisition cohort."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Customer-Lifetime-Value/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Customer-Lifetime-Value",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Customer-Lifetime-Value/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Customer-Lifetime-Value/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Customer-Lifetime-Value/sample-data/orders.csv"
    ]
   }
  },
  {
   "id": "Analytics/Data-Profiler",
   "name": "Data Profiler",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": null,
   "description": "Profiles a dataset: one record per field with its type, missing and distinct counts, most common value and statistics, and the records with z-scores and anomaly flags for every numeric field.",
   "tags": [
    "profiling",
    "data profile",
    "field statistics",
    "data quality",
    "outliers",
    "anomaly",
    "z-score",
    "missing values"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Fields to profile",
     "type": "Fields",
     "description": "Leave empty to profile every field."
    }
   ],
   "options": [
    {
     "name": "threshold",
     "title": "Anomaly threshold (standard deviations)",
     "type": "Number",
     "description": "A numeric value is flagged when it is at least this many standard deviations from its field's mean. 3 is usual."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Fields",
     "description": "One record per field: type, missing and distinct counts, most common value, statistics for numbers, first and last date for dates."
    },
    {
     "id": "Output Data 2",
     "label": "Records",
     "description": "The input records with a z-score, an anomaly flag and a p-value for each numeric field, and an Anomaly count."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block puts a first-look profile into the workflow as data, with the same fields every time, so **Validate data** can check it. Unlike the core **Anomaly Detection** block, which scores whole records, it flags each field separately, showing which value is unusual.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Field metadata",
     "page": null,
     "note": "Lists chosen metadata for each field, one record per field. This block adds the most common value, quartiles and skewness, and scores every numeric value with a z-score and anomaly flag."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Outlier-Flagger",
     "name": "Outlier Flagger",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Outlier-Flagger/index.html",
     "note": "IQR and median-based (MAD) outlier rules, within groups, for skewed fields where a z-score misleads."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "An issue log of duplicate rows, missing values and IQR outliers."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Histogram-Distribution",
     "name": "Histogram and Distribution",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Histogram-Distribution/index.html",
     "note": "Histograms and percentiles for one numeric field, compared across groups."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Data-Profiler/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Data-Profiler",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Data-Profiler/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Data-Profiler/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Data-Profiler/sample-data/customer-orders.csv"
    ]
   }
  },
  {
   "id": "Analytics/Find-Nearest",
   "name": "Find Nearest",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Geo",
   "description": "Finds the k nearest points in a second input for each point in the first, by great-circle distance, with an optional distance limit and a catchment summary per destination.",
   "tags": [
    "nearest",
    "distance",
    "spatial",
    "geo",
    "catchment",
    "latitude",
    "longitude",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "origin_lat",
     "title": "Latitude",
     "type": "Field",
     "description": "Latitude of each point in input 1, in decimal degrees."
    },
    {
     "name": "origin_lon",
     "title": "Longitude",
     "type": "Field",
     "description": "Longitude of each point in input 1, in decimal degrees."
    },
    {
     "name": "dest_lat",
     "title": "Latitude",
     "type": "Field",
     "description": "Latitude of each candidate in input 2, in decimal degrees."
    },
    {
     "name": "dest_lon",
     "title": "Longitude",
     "type": "Field",
     "description": "Longitude of each candidate in input 2, in decimal degrees."
    },
    {
     "name": "dest_id",
     "title": "Name or ID (for the summary)",
     "type": "Field",
     "description": "Optional field naming each destination in the catchment summary."
    }
   ],
   "options": [
    {
     "name": "k",
     "title": "Number of nearest points",
     "type": "Whole number",
     "description": "1 for the nearest only; more gives one row per match, ranked."
    },
    {
     "name": "max_distance",
     "title": "Maximum distance",
     "type": "Number",
     "description": "0 = no limit."
    },
    {
     "name": "unit",
     "title": "Distance unit",
     "type": "Choice",
     "description": "Unit for distances and the maximum distance."
    },
    {
     "name": "keep_unmatched",
     "title": "Keep points with no match",
     "type": "Yes / no",
     "description": "Keep points with no destination in range (or invalid coordinates), with an empty match."
    }
   ],
   "outputs": [
    {
     "id": "nearest",
     "label": "Nearest",
     "description": "Input 1 records with the matched destination's fields, Nearest Rank, distance, bearing, direction and match status."
    },
    {
     "id": "catchments",
     "label": "Catchments",
     "description": "One row per destination: points nearest to it, mean / median / furthest distance, share of points."
    }
   ],
   "libraries": [],
   "relatedSummary": "The LAT_LON_DISTANCE formula measures one pair of points, so finding the nearest with it needs a cross join first, which does not scale. This block finds the k nearest points in a second input directly, within an optional maximum distance.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Trade-Area-Summary",
     "name": "Trade Area Summary",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Trade-Area-Summary/index.html",
     "note": "Counts and sums points within several radii of each site, with shared catchment."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Point-To-Point-Distance",
     "name": "Point-to-Point Distance",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-To-Point-Distance/index.html",
     "note": "Distance and bearing between two locations on the same row."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Find-Nearest/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Find-Nearest",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Find-Nearest/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Find-Nearest/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Find-Nearest/sample-data/customers.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Find-Nearest/sample-data/stores.csv"
    ]
   }
  },
  {
   "id": "Analytics/Forecast-Accuracy",
   "name": "Forecast Accuracy",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Supply Chain",
   "description": "Compares forecasts with actuals per item and model: MAE, RMSE, MAPE, sMAPE, WAPE, bias and tracking signal, with flags, best model per item and a model ranking.",
   "tags": [
    "forecast accuracy",
    "wape",
    "mape",
    "bias",
    "tracking signal",
    "demand planning",
    "supply chain",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "actual_field",
     "title": "Actual field",
     "type": "Field",
     "description": "The value that actually happened, such as units sold."
    },
    {
     "name": "forecast_fields",
     "title": "Forecast fields",
     "type": "Fields",
     "description": "One or more forecasts (models) to evaluate."
    },
    {
     "name": "item_fields",
     "title": "Item fields (optional)",
     "type": "Fields",
     "description": "For example SKU, or SKU and Location."
    },
    {
     "name": "period_field",
     "title": "Period field (optional)",
     "type": "Field",
     "description": "Orders rows for the tracking signal."
    }
   ],
   "options": [
    {
     "name": "best_by",
     "title": "Pick the best model by",
     "type": "Choice",
     "description": "The measure used to pick each item's best model and to rank the models."
    },
    {
     "name": "bias_limit",
     "title": "Bias limit",
     "type": "Number",
     "description": "Flag items whose bias exceeds this (0.1 = 10%)."
    },
    {
     "name": "tracking_limit",
     "title": "Tracking signal limit",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "items",
     "label": "By item",
     "description": "Per item and model: periods, totals, MAE, RMSE, MAPE, sMAPE, WAPE, accuracy, bias, tracking signal, flags, rank, best model."
    },
    {
     "id": "models",
     "label": "By model",
     "description": "Per model: the same measures over all items, items where best, rank."
    },
    {
     "id": "errors",
     "label": "Errors",
     "description": "Per item, period and model: actual, forecast, error, absolute and percentage error."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block scores any forecast against actuals, including those from the core **Time Series Forecast** block, with MAE, RMSE, MAPE, WAPE, bias and tracking signal per item.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Holt-Winters-Forecast",
     "name": "Holt-Winters Forecast",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Holt-Winters-Forecast/index.html",
     "note": "Produces forecasts per series that this block can score against actuals."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Validation/Model-Validation",
     "name": "Model Validation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Validation/Model-Validation/index.html",
     "note": "The equivalent for predicted categories: confusion matrix, precision and recall."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Forecast-Accuracy/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Forecast-Accuracy",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Forecast-Accuracy/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Forecast-Accuracy/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Forecast-Accuracy/sample-data/weekly-forecasts.csv"
    ]
   }
  },
  {
   "id": "Analytics/Funnel-Analysis",
   "name": "Funnel Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "Builds conversion funnels from event data: users reaching each ordered step within an optional time window, conversion, drop-off and time between steps, per segment.",
   "tags": [
    "funnel",
    "conversion",
    "drop-off",
    "events",
    "web analytics",
    "product analytics",
    "e-commerce",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "user_field",
     "title": "User field",
     "type": "Field",
     "description": "Identifies the person or session moving through the funnel."
    },
    {
     "name": "event_field",
     "title": "Event name field",
     "type": "Field",
     "description": "Holds the event names listed in the funnel steps."
    },
    {
     "name": "time_field",
     "title": "Event time field",
     "type": "Field",
     "description": "When each event happened."
    },
    {
     "name": "segment_fields",
     "title": "Segment by (optional)",
     "type": "Fields",
     "description": "For example Device or Channel."
    }
   ],
   "options": [
    {
     "name": "steps",
     "title": "Funnel steps, in order",
     "type": "Text",
     "description": "Event names separated by commas or ->, for example: view_product, add_to_basket, begin_checkout, purchase"
    },
    {
     "name": "ordered",
     "title": "Steps must happen in order",
     "type": "Yes / no",
     "description": "Ticked: a step counts only after the previous one. Unticked: any order."
    },
    {
     "name": "window_hours",
     "title": "Time window (hours)",
     "type": "Number",
     "description": "All steps within this many hours of the first step. 0 = no limit."
    }
   ],
   "outputs": [
    {
     "id": "funnel",
     "label": "Funnel",
     "description": "Per segment and step: users, conversion from start and previous step, drop-off, median hours from previous step."
    },
    {
     "id": "users",
     "label": "Users",
     "description": "Per user: segment, steps reached, furthest step, converted, step times, hours to convert."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks a funnel takes a pivot of first-event times per user and step-by-step formulas; this block handles the step order and time window for you.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "eventfunnel",
     "name": "Event Funnel",
     "page": "https://visokio.github.io/omniscope-custom-views/eventfunnel/index.html",
     "note": "Follows users through ordered steps from the same kind of event log, in a report, and selects those who dropped off. Use this block for the per-user table (furthest step, hours to convert) or for a funnel where steps may happen in any order."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Path-Analysis",
     "name": "Path Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Path-Analysis/index.html",
     "note": "The most common event sequences, for when the steps are not known in advance."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Funnel-Analysis/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Funnel-Analysis",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Funnel-Analysis/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Funnel-Analysis/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Funnel-Analysis/sample-data/web-events.csv"
    ]
   }
  },
  {
   "id": "Analytics/Histogram-Distribution",
   "name": "Histogram and Distribution",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Histograms with automatic bin widths shared across groups, and distribution statistics (percentiles, skewness, kurtosis) per group.",
   "tags": [
    "histogram",
    "distribution",
    "binning",
    "percentiles",
    "skewness",
    "descriptive statistics",
    "Freedman-Diaconis",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "The numbers to bin and describe."
    },
    {
     "name": "group_field",
     "title": "Per group (optional)",
     "type": "Field",
     "description": "One histogram per value, all on the same bins so they can be compared."
    }
   ],
   "options": [
    {
     "name": "bin_rule",
     "title": "Bin width rule",
     "type": "Choice",
     "description": "How the bin width is chosen when you do not set one. Freedman-Diaconis copes better with outliers."
    },
    {
     "name": "bins",
     "title": "Number of bins (optional)",
     "type": "Whole number",
     "description": "0 = use the rule."
    },
    {
     "name": "bin_width",
     "title": "Bin width (optional)",
     "type": "Number",
     "description": "0 = use the rule or the number of bins."
    },
    {
     "name": "clip_percentile",
     "title": "Clip range to percentiles",
     "type": "Number",
     "description": "For example 1 = from P1 to P99; 0 = full range."
    }
   ],
   "outputs": [
    {
     "id": "histogram",
     "label": "Histogram",
     "description": "Per group and bin: from, to, midpoint, count, share, cumulative share."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per group: count, missing, mean, standard deviation, percentiles, skewness, kurtosis, below / above range."
    }
   ],
   "libraries": [],
   "relatedSummary": "Report charts can draw a histogram; this block outputs bins shared across groups, so groups compare like for like.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "distribution",
     "name": "Distribution",
     "page": "https://visokio.github.io/omniscope-custom-views/distribution/index.html",
     "note": "Draws the histogram with median, percentiles and outliers in a report, comparing groups as outlines on a shared scale. Use this block when the bins and the statistics are needed as tables."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Data-Profiler",
     "name": "Data Profiler",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Data-Profiler/index.html",
     "note": "Per-field statistics including counts, mean, nulls and skewness, but no histograms or per-group comparison."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Field metadata",
     "page": null,
     "note": "One record per field with counts and summaries."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Tile",
     "name": "Tile",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Tile/index.html",
     "note": "Gives each record its band (equal width, quantile and more) rather than counting bins."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Histogram-Distribution/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Histogram-Distribution",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Histogram-Distribution/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Histogram-Distribution/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Histogram-Distribution/sample-data/order-values.csv"
    ]
   }
  },
  {
   "id": "Analytics/Holt-Winters-Forecast",
   "name": "Holt-Winters Forecast",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Time Series",
   "description": "Exponential smoothing forecasts per series with automatic trend and seasonality selection, prediction intervals and a holdout accuracy check.",
   "tags": [
    "forecast",
    "Holt-Winters",
    "ETS",
    "exponential smoothing",
    "seasonality",
    "prediction intervals",
    "demand planning",
    "time series",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each value; values are summed per period."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "The quantity to forecast, such as sales."
    },
    {
     "name": "series_fields",
     "title": "One forecast per (optional)",
     "type": "Fields",
     "description": "For example Product or Store."
    }
   ],
   "options": [
    {
     "name": "period",
     "title": "Period",
     "type": "Choice"
    },
    {
     "name": "horizon",
     "title": "Horizon (periods)",
     "type": "Whole number",
     "description": "How many periods to forecast past the last one in the data."
    },
    {
     "name": "season_length",
     "title": "Season length",
     "type": "Whole number",
     "description": "0 = default for the period (12 months, 52 weeks, 7 days, 4 quarters)."
    },
    {
     "name": "trend",
     "title": "Trend",
     "type": "Choice",
     "description": "Automatic picks the trend with the best fit; a damped trend levels off over the horizon."
    },
    {
     "name": "seasonality",
     "title": "Seasonality",
     "type": "Choice",
     "description": "Automatic picks the best fit. Multiplicative seasons grow with the level; additive ones stay the same size."
    },
    {
     "name": "holdout",
     "title": "Holdout check (periods)",
     "type": "Whole number",
     "description": "0 = no check."
    },
    {
     "name": "simulations",
     "title": "Simulated paths for intervals",
     "type": "Whole number"
    },
    {
     "name": "seed",
     "title": "Random seed",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "forecast",
     "label": "Forecast",
     "description": "Per series and period: row type (Actual, Holdout forecast, Forecast), actual, fitted, forecast, 80% and 95% intervals, horizon."
    },
    {
     "id": "models",
     "label": "Models",
     "description": "Per series: model chosen, smoothing parameters, AICc, in-sample error, holdout accuracy and MASE."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Time Series Forecast** block suits general forecasting. This block uses one transparent, well-known method per series, chooses trend and seasonality automatically and checks accuracy on a holdout, for teams who need to explain or match an established method.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Time Series Forecast",
     "page": null,
     "note": "Predicts future values of a measure from past observations. This block uses Holt-Winters exponential smoothing per series with prediction intervals."
    },
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "forecast",
     "name": "Forecast",
     "page": "https://visokio.github.io/omniscope-custom-views/forecast/index.html",
     "note": "Fits Holt-Winters to one series in a report, with 80% and 95% ranges and a backtest, and re-fits it for any filtered slice. Use this block to forecast many series in one pass and keep the forecasts and their accuracy as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Forecast-Accuracy",
     "name": "Forecast Accuracy",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Forecast-Accuracy/index.html",
     "note": "Compares forecasts from several models with actuals."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Seasonal-Decomposition",
     "name": "Seasonal Decomposition",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Seasonal-Decomposition/index.html",
     "note": "Splits the history into trend and seasonal parts, to understand it rather than project it."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Stock-Run-Out",
     "name": "Stock Run-Out Projection",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Stock-Run-Out/index.html",
     "note": "Projects stock from a demand forecast such as this one."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Area Range",
     "page": null,
     "note": "Draws the forecast with its 80% or 95% interval as a band."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Holt-Winters-Forecast/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Holt-Winters-Forecast",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Holt-Winters-Forecast/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Holt-Winters-Forecast/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Holt-Winters-Forecast/sample-data/monthly-sales.csv"
    ]
   }
  },
  {
   "id": "Analytics/Inflation-Adjustment",
   "name": "Inflation Adjustment",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Restates money values in real terms of a base period using a price index table, per series, with the index used and cumulative inflation.",
   "tags": [
    "inflation",
    "real terms",
    "CPI",
    "price index",
    "constant prices",
    "deflator",
    "finance",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each value, matched to the index period it falls in."
    },
    {
     "name": "value_fields",
     "title": "Money fields to adjust",
     "type": "Fields",
     "description": "Money amounts to restate in base-period prices; each gets a new real-terms field."
    },
    {
     "name": "key_field",
     "title": "Key (optional)",
     "type": "Field",
     "description": "For example Country, matched to the index key."
    },
    {
     "name": "index_date_field",
     "title": "Index period",
     "type": "Field",
     "description": "The month or date each index value applies to."
    },
    {
     "name": "index_value_field",
     "title": "Index value",
     "type": "Field",
     "description": "The price index, such as CPI."
    },
    {
     "name": "index_key_field",
     "title": "Index key (optional)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "base_period",
     "title": "Base period",
     "type": "Text",
     "description": "A date or month, for example 2025-06. Default: latest index period."
    },
    {
     "name": "max_age_days",
     "title": "Maximum index age (days)",
     "type": "Whole number"
    },
    {
     "name": "suffix",
     "title": "Suffix for real-terms fields",
     "type": "Text"
    }
   ],
   "outputs": [
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with index period used, index at date and at base, inflation factor, real values and status."
    },
    {
     "id": "index",
     "label": "Index",
     "description": "The index table with inflation to base, month-on-month and year-on-year change."
    }
   ],
   "libraries": [],
   "relatedSummary": "Restating amounts is possible with a join to an index table and a formula. This block also chooses the right index per series and reports the index used and the cumulative inflation.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Price-Index",
     "name": "Price Index",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Index/index.html",
     "note": "Builds your own price index from item prices and quantities, which this block can then use."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/As-Of-Join",
     "name": "As-Of Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/As-Of-Join/index.html",
     "note": "General join to the record in force at each date; this block does that for index values and then restates the amounts."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Inflation-Adjustment/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Inflation-Adjustment",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Inflation-Adjustment/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Inflation-Adjustment/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Inflation-Adjustment/sample-data/price-index.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Inflation-Adjustment/sample-data/salaries.csv"
    ]
   }
  },
  {
   "id": "Analytics/Investment-Appraisal",
   "name": "Investment Appraisal",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "NPV, IRR / XIRR, MIRR, payback, discounted payback and profitability index per project from cash flows, with NPV profiles and multiple-IRR warnings.",
   "tags": [
    "NPV",
    "IRR",
    "XIRR",
    "MIRR",
    "payback",
    "capital budgeting",
    "business case",
    "discounted cash flow",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "project_field",
     "title": "Project field",
     "type": "Field",
     "description": "Names the project each cash flow belongs to."
    },
    {
     "name": "time_field",
     "title": "Period or date field",
     "type": "Field",
     "description": "Period number (0 = today) or the date of each cash flow, to match Timing."
    },
    {
     "name": "amount_field",
     "title": "Cash flow field",
     "type": "Field",
     "description": "Outflows negative, inflows positive."
    },
    {
     "name": "rate_field",
     "title": "Rate per project (optional)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "timing",
     "title": "Timing",
     "type": "Choice",
     "description": "Period numbers for NPV and IRR; dates for XNPV and XIRR on actual days."
    },
    {
     "name": "discount_rate",
     "title": "Discount rate",
     "type": "Number",
     "description": "Per period, or annual for dates. For example 0.08."
    },
    {
     "name": "profile_max_rate",
     "title": "NPV profile up to rate",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "projects",
     "label": "Projects",
     "description": "Per project: rate, flows, NPV, IRR, MIRR, profitability index, payback, discounted payback, notes, decision, rank."
    },
    {
     "id": "cashflows",
     "label": "Cash flows",
     "description": "Per project and time: cash flow, discount factor, present value, cumulative cash flow and present value."
    },
    {
     "id": "npv_profile",
     "label": "NPV profile",
     "description": "Per project: NPV at rates from 0% to the maximum in 1% steps."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Investment-Appraisal/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Investment-Appraisal",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Investment-Appraisal/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Investment-Appraisal/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Investment-Appraisal/sample-data/capital-requests.csv"
    ]
   }
  },
  {
   "id": "Analytics/Likert-Survey",
   "name": "Likert Survey Summary",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Survey",
   "description": "Summarises Likert-scale survey questions: answer distributions, mean scores, top-2-box, net scores and neutral shares, per question and segment.",
   "tags": [
    "survey",
    "likert",
    "engagement",
    "satisfaction",
    "top 2 box",
    "net score",
    "questionnaire",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "question_fields",
     "title": "Question fields",
     "type": "Fields",
     "description": "One field per survey statement."
    },
    {
     "name": "segment_fields",
     "title": "Break down by (optional)",
     "type": "Fields",
     "description": "For example Department or Region."
    }
   ],
   "options": [
    {
     "name": "scale",
     "title": "Answer scale",
     "type": "Choice",
     "description": "How the answers are worded, lowest to highest. Numbers and custom labels cover other scales."
    },
    {
     "name": "custom_labels",
     "title": "Custom labels (lowest first)",
     "type": "Text",
     "description": "Comma-separated, for example Poor, Fair, Good, Very good, Excellent."
    },
    {
     "name": "box_size",
     "title": "Top / bottom box size",
     "type": "Whole number",
     "description": "2 = top-2-box (Agree + Strongly agree)."
    },
    {
     "name": "min_responses",
     "title": "Minimum responses to report reliably",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "summary",
     "label": "Questions",
     "description": "Per question (and segment): responses, mean, top / bottom box, neutral, net score, rank, reliability."
    },
    {
     "id": "distribution",
     "label": "Answers",
     "description": "Per question (and segment) and answer: count, share, diverging share."
    },
    {
     "id": "responses",
     "label": "Responses",
     "description": "One row per respondent and question: answer label and score."
    }
   ],
   "libraries": [],
   "relatedSummary": "The standard survey summary (distributions, top-2-box, net scores) is possible with a de-pivot, aggregates and formulas; this block produces it in one step, from wide or long survey data.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "likert",
     "name": "Likert Survey",
     "page": "https://visokio.github.io/omniscope-custom-views/likert/index.html",
     "note": "Draws the diverging chart from answers in long layout (one row per answer), which this block's Responses (long) output provides. Use this block for mean scores, top-2-box and net scores as a table, or to start from a wide survey export."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Net-Promoter-Score",
     "name": "Net Promoter Score",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Net-Promoter-Score/index.html",
     "note": "NPS from 0 to 10 recommend answers, with margins of error."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Likert-Survey/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Likert-Survey",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Likert-Survey/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Likert-Survey/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Likert-Survey/sample-data/engagement-survey.csv"
    ]
   }
  },
  {
   "id": "Analytics/Linear-Optimisation",
   "name": "Linear Optimisation",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Prescriptive",
   "description": "Solves linear and integer programmes given as data (variables with objective and bounds, constraints in long format) with the HiGHS solver: optimal plan, slack, binding constraints and shadow prices.",
   "tags": [
    "optimisation",
    "linear programming",
    "integer programming",
    "product mix",
    "allocation",
    "prescriptive",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "var_name_field",
     "title": "Variable name",
     "type": "Field",
     "description": "Names each decision variable, such as a product to make."
    },
    {
     "name": "objective_field",
     "title": "Objective coefficient",
     "type": "Field",
     "description": "Profit or cost per unit."
    },
    {
     "name": "lower_field",
     "title": "Lower bound (optional)",
     "type": "Field",
     "description": "Minimum value; empty means 0."
    },
    {
     "name": "upper_field",
     "title": "Upper bound (optional)",
     "type": "Field",
     "description": "Maximum value; empty means no limit."
    },
    {
     "name": "integer_field",
     "title": "Whole-number flag (optional)",
     "type": "Field",
     "description": "Yes / true / 1 for variables that must be whole numbers."
    },
    {
     "name": "constraint_field",
     "title": "Constraint name",
     "type": "Field",
     "description": "Names each constraint, such as a resource; one row per variable that uses it."
    },
    {
     "name": "constraint_var_field",
     "title": "Variable",
     "type": "Field",
     "description": "Must match the variable names in input 1."
    },
    {
     "name": "coefficient_field",
     "title": "Coefficient",
     "type": "Field",
     "description": "Amount of the constraint used by one unit of the variable."
    },
    {
     "name": "sense_field",
     "title": "Sense",
     "type": "Field",
     "description": "<=, >= or = (also: at most, at least, exactly)."
    },
    {
     "name": "limit_field",
     "title": "Limit",
     "type": "Field",
     "description": "Right-hand side, such as capacity."
    }
   ],
   "options": [
    {
     "name": "objective_sense",
     "title": "Goal",
     "type": "Choice",
     "description": "Whether the best plan has the highest or the lowest objective."
    },
    {
     "name": "all_integer",
     "title": "All variables are whole numbers",
     "type": "Yes / no"
    },
    {
     "name": "time_limit",
     "title": "Time limit (seconds)",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "solution",
     "label": "Solution",
     "description": "Input 1 with Optimal Value, Objective Contribution, At Bound, Reduced Cost."
    },
    {
     "id": "constraints",
     "label": "Constraints",
     "description": "Per constraint: sense, limit, used, slack, binding, shadow price."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Status, objective value, variable and constraint counts, solver message."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Linear-Optimisation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Linear-Optimisation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Linear-Optimisation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Linear-Optimisation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Linear-Optimisation/sample-data/products.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Linear-Optimisation/sample-data/resource-usage.csv"
    ]
   }
  },
  {
   "id": "Analytics/Loan-Amortisation",
   "name": "Loan Amortisation Schedule",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Generates payment schedules for loans and leases (repayment, equal principal, interest only, with balloon), monthly, quarterly or annually, with a summary per loan.",
   "tags": [
    "amortisation",
    "loan",
    "mortgage",
    "lease",
    "finance",
    "schedule",
    "interest",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "loan_id_field",
     "title": "Loan ID (optional)",
     "type": "Field",
     "description": "Names each loan in the schedule and summary."
    },
    {
     "name": "principal_field",
     "title": "Principal",
     "type": "Field",
     "description": "Amount borrowed."
    },
    {
     "name": "rate_field",
     "title": "Annual interest rate",
     "type": "Field",
     "description": "Percent (7.9) or fraction (0.079)."
    },
    {
     "name": "term_field",
     "title": "Term",
     "type": "Field",
     "description": "Length of the loan, in the unit set by Term is in."
    },
    {
     "name": "start_field",
     "title": "Start date (optional)",
     "type": "Field",
     "description": "Payments fall one period after this date, then every period."
    },
    {
     "name": "balloon_field",
     "title": "Balloon / residual (optional)",
     "type": "Field",
     "description": "Amount left to pay at the end, as in a lease or balloon loan."
    },
    {
     "name": "type_field",
     "title": "Repayment type field (optional)",
     "type": "Field",
     "description": "Per-loan type; otherwise the default below is used."
    }
   ],
   "options": [
    {
     "name": "repayment_type",
     "title": "Default repayment type",
     "type": "Choice"
    },
    {
     "name": "frequency",
     "title": "Payment frequency",
     "type": "Choice",
     "description": "How often payments fall due."
    },
    {
     "name": "term_unit",
     "title": "Term is in",
     "type": "Choice",
     "description": "Whether the term field counts months or years."
    },
    {
     "name": "rate_format",
     "title": "Rate format",
     "type": "Choice"
    }
   ],
   "outputs": [
    {
     "id": "schedule",
     "label": "Schedule",
     "description": "One row per loan and payment: date, opening balance, payment, interest, principal repaid, closing balance, cumulative interest."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "One row per loan: type, status, payments, regular / first / final payment, total paid, total interest, final payment date."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Loan-Amortisation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Loan-Amortisation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Loan-Amortisation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Loan-Amortisation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Loan-Amortisation/sample-data/loan-book.csv"
    ]
   }
  },
  {
   "id": "Analytics/Monte-Carlo-Cost-Risk",
   "name": "Monte Carlo Cost Risk",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Simulates three-point estimates and risk events to give P10 / P50 / P80 / P90 totals, contingency, the probability of exceeding a budget, and the drivers of uncertainty.",
   "tags": [
    "Monte Carlo",
    "cost risk",
    "simulation",
    "PERT",
    "contingency",
    "risk register",
    "P80",
    "project estimate",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "name_field",
     "title": "Line name",
     "type": "Field",
     "description": "Names each estimate line or risk."
    },
    {
     "name": "low_field",
     "title": "Low estimate",
     "type": "Field",
     "description": "Optimistic cost of the line."
    },
    {
     "name": "likely_field",
     "title": "Most likely estimate",
     "type": "Field",
     "description": "Most likely cost of the line."
    },
    {
     "name": "high_field",
     "title": "High estimate",
     "type": "Field",
     "description": "Pessimistic cost of the line."
    },
    {
     "name": "probability_field",
     "title": "Probability (optional)",
     "type": "Field",
     "description": "For risk events; empty = always occurs."
    },
    {
     "name": "group_field",
     "title": "Group (optional)",
     "type": "Field",
     "description": "For example work package."
    }
   ],
   "options": [
    {
     "name": "distribution",
     "title": "Distribution",
     "type": "Choice",
     "description": "Shape of each line's range. PERT weights the most likely value; triangular gives more weight to the extremes."
    },
    {
     "name": "iterations",
     "title": "Iterations",
     "type": "Whole number",
     "description": "Number of simulated totals. More gives steadier percentiles."
    },
    {
     "name": "correlation",
     "title": "Correlation between lines",
     "type": "Number",
     "description": "0 = independent; 0.3 to 0.5 is common for related costs."
    },
    {
     "name": "seed",
     "title": "Random seed",
     "type": "Whole number",
     "description": "Fixes the random numbers, so a rerun gives the same results."
    },
    {
     "name": "target",
     "title": "Target or budget (optional)",
     "type": "Number",
     "description": "Reports the chance of exceeding it."
    },
    {
     "name": "bins",
     "title": "S-curve bins",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "summary",
     "label": "Summary",
     "description": "Total and per group: base, mean, P10-P95, contingency to P50 / P80 / P90, probability over base (and target)."
    },
    {
     "id": "drivers",
     "label": "Drivers",
     "description": "Per line: inputs, simulated mean, spread, P10 / P90, rank correlation with the total, share of variance, rank."
    },
    {
     "id": "distribution",
     "label": "S-curve",
     "description": "Histogram of the total with cumulative probability."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Distribution Input** block is a building block for simulation; this block runs the whole cost-risk simulation: three-point estimates, risk events, P-values, contingency and the lines that drive the risk.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Distribution Input",
     "page": null,
     "note": "Generates random values from a chosen distribution."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Monte-Carlo-Cost-Risk/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Monte-Carlo-Cost-Risk",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Monte-Carlo-Cost-Risk/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Monte-Carlo-Cost-Risk/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Monte-Carlo-Cost-Risk/sample-data/project-estimate.csv"
    ]
   }
  },
  {
   "id": "Analytics/Net-Promoter-Score",
   "name": "Net Promoter Score",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "Net Promoter Score per group and period from 0-10 answers, with margin of error, confidence interval, weights, significance vs the rest and vs the previous period.",
   "tags": [
    "NPS",
    "net promoter score",
    "survey",
    "customer experience",
    "CX",
    "voice of the customer",
    "confidence interval",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "score_field",
     "title": "Score field (0-10)",
     "type": "Field",
     "description": "The answer to the likelihood-to-recommend question, from 0 to 10."
    },
    {
     "name": "group_fields",
     "title": "Group by (optional)",
     "type": "Fields",
     "description": "For example Region or Channel."
    },
    {
     "name": "weight_field",
     "title": "Weight field (optional)",
     "type": "Field",
     "description": "Survey weights; leave empty for unweighted."
    },
    {
     "name": "date_field",
     "title": "Date field (optional)",
     "type": "Field",
     "description": "Needed to report by period."
    }
   ],
   "options": [
    {
     "name": "period",
     "title": "Report by period",
     "type": "Choice",
     "description": "Adds a row per period, compared with the period before. Needs a date field."
    },
    {
     "name": "confidence",
     "title": "Confidence level",
     "type": "Number",
     "description": "For the margin of error and significance tests."
    },
    {
     "name": "min_responses",
     "title": "Small sample below",
     "type": "Whole number",
     "description": "Rows with fewer responses are flagged."
    }
   ],
   "outputs": [
    {
     "id": "nps",
     "label": "NPS",
     "description": "Per group (and period): counts, shares, NPS, standard error, margin of error, confidence interval, vs rest, change vs previous period."
    },
    {
     "id": "distribution",
     "label": "Distribution",
     "description": "Per group: responses and share for each score 0 to 10."
    },
    {
     "id": "responses",
     "label": "Responses",
     "description": "The input with NPS Category and NPS Points (+100 / 0 / -100)."
    }
   ],
   "libraries": [],
   "relatedSummary": "A basic NPS is possible with formulas and an Aggregate. This block adds the margin of error, confidence intervals, weights and period comparison.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Likert-Survey",
     "name": "Likert Survey Summary",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Likert-Survey/index.html",
     "note": "Summaries of agreement or satisfaction scale questions from the same kind of survey export."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "likert",
     "name": "Likert Survey",
     "page": "https://visokio.github.io/omniscope-custom-views/likert/index.html",
     "note": "Shows 0 to 10 answers as a Net Promoter Score chart, split by group."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Net-Promoter-Score/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Net-Promoter-Score",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Net-Promoter-Score/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Net-Promoter-Score/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Net-Promoter-Score/sample-data/nps-survey.csv"
    ]
   }
  },
  {
   "id": "Analytics/Network-Analysis/Attribute-Analysis",
   "name": "Attribute Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Network Analysis",
   "description": "From a list of connections (each record: a node, the node it connects to, and the first node's category), measures how much each category's connections lead to nodes it shares with each other category, such as which departments work on the same projects.",
   "tags": [
    "attribute",
    "analysis",
    "network",
    "graph",
    "co-occurrence",
    "shared connections"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "from",
     "title": "From field",
     "type": "Field",
     "description": "The node each connection starts from: the one that has the category (for example Person)."
    },
    {
     "name": "to",
     "title": "To field",
     "type": "Field",
     "description": "The node each connection leads to (for example Project). Categories are compared through the To nodes they share."
    },
    {
     "name": "category",
     "title": "Category field",
     "type": "Field",
     "description": "The From node's category (for example Department)."
    }
   ],
   "options": [],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Category links",
     "description": "One record per pair of categories that share To nodes: From category, To category, Probability (share of the From category's connections that lead to nodes shared with the To category) and Connections (the From category's number of connections)."
    }
   ],
   "libraries": [],
   "relatedSummary": "Omniscope's **Network** view draws the connections node by node, and an Aggregate block can count connections per category but not which categories meet at the same nodes. This block rolls the network up to its categories, weighted so large groups do not dominate.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Network Analysis",
     "page": null,
     "note": "Finds important nodes and clusters in a link table."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Network-Analysis/TSNE",
     "name": "TSNE",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Network-Analysis/TSNE/index.html",
     "note": "Maps the nodes of the same kind of connection list in 2-D, by who they connect to."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "networkanalyser",
     "name": "Network Analyser",
     "page": "https://visokio.github.io/omniscope-custom-views/networkanalyser/index.html",
     "note": "Can draw the category links as a directed network, with Probability as the link weight."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Network-Analysis/Attribute-Analysis/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Network-Analysis/Attribute-Analysis",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Network-Analysis/Attribute-Analysis/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Network-Analysis/Attribute-Analysis/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Network-Analysis/Attribute-Analysis/sample-data/project-assignments.csv"
    ]
   }
  },
  {
   "id": "Analytics/Network-Analysis/TSNE",
   "name": "TSNE",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Network Analysis",
   "description": "Places the nodes of a network on a 2-D (or 1-D or 3-D) map with t-SNE, so that nodes connected to the same other nodes end up close together and nodes with different connections far apart. Input: one record per connection.",
   "tags": [
    "relationship",
    "analysis",
    "network",
    "graph",
    "t-sne",
    "tsne",
    "layout",
    "dimensionality reduction"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "from",
     "title": "From field",
     "type": "Field",
     "description": "One end of each connection (for example the sender)."
    },
    {
     "name": "to",
     "title": "To field",
     "type": "Field",
     "description": "The other end of each connection (for example the recipient). Connections count in both directions."
    },
    {
     "name": "weight",
     "title": "Weight field",
     "type": "Field",
     "description": "Optional numeric field: how strong each connection is (for example a number of messages). Repeated connections between the same two nodes are added up. Without it every connection counts 1."
    }
   ],
   "options": [
    {
     "name": "outputDimensions",
     "title": "Number of output dimensions",
     "type": "Whole number",
     "description": "How many coordinates each node gets: 2 for a scatter chart, 1 for an ordering, 3 for a 3-D view."
    },
    {
     "name": "initialDimensions",
     "title": "PCA dimensions",
     "type": "Whole number",
     "description": "Before t-SNE, each node's connection profile is reduced to this many dimensions with principal component analysis. More keeps more detail but is slower."
    },
    {
     "name": "perplexity",
     "title": "Perplexity",
     "type": "Whole number",
     "description": "Roughly how many neighbours each node takes into account. Lower values bring out small, local groups; higher values the overall structure. Must be at most (number of nodes - 1) / 3."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Map",
     "description": "One record per node: Node, Dimension 1, Dimension 2 (and 3) and Connections (the number of connection records it is part of)."
    }
   ],
   "libraries": [],
   "relatedSummary": "Omniscope's **Network** view draws nodes and links directly. This block instead computes a 2-D position for every node from its pattern of connections and outputs it as data; links are not drawn, which keeps large, dense networks readable.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Network Analysis",
     "page": null,
     "note": "Finds important nodes and clusters in a link table."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Network-Analysis/Attribute-Analysis",
     "name": "Attribute Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Network-Analysis/Attribute-Analysis/index.html",
     "note": "Measures how much each category's connections overlap with each other category's, from the same kind of input."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "forcecoordinategraph",
     "name": "Force Coordinate Graph",
     "page": "https://visokio.github.io/omniscope-custom-views/forcecoordinategraph/index.html",
     "note": "Draws a network at X and Y positions you supply: join this block's map back onto the connections to show the links between the placed nodes."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Scatter",
     "page": null,
     "note": "Plots the map, Dimension 1 against Dimension 2, one point per node."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Network-Analysis/TSNE/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Network-Analysis/TSNE",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Network-Analysis/TSNE/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Network-Analysis/TSNE/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Network-Analysis/TSNE/sample-data/email-network.csv"
    ]
   }
  },
  {
   "id": "Analytics/Occupancy-From-Intervals",
   "name": "Occupancy From Intervals",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Operations",
   "description": "Occupancy over time from start and end times: average and peak concurrency per bucket, utilisation against capacity and time over capacity, per group.",
   "tags": [
    "occupancy",
    "concurrency",
    "utilisation",
    "capacity",
    "bed management",
    "car parks",
    "bookings",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Start time",
     "type": "Field",
     "description": "When each stay, booking or session began."
    },
    {
     "name": "end_field",
     "title": "End time",
     "type": "Field",
     "description": "Empty = still in progress."
    },
    {
     "name": "group_field",
     "title": "Group (optional)",
     "type": "Field",
     "description": "For example Ward or Car Park."
    },
    {
     "name": "weight_field",
     "title": "Weight (optional)",
     "type": "Field",
     "description": "For example people in a booking."
    }
   ],
   "options": [
    {
     "name": "as_of",
     "title": "As of (for open intervals)",
     "type": "Text",
     "description": "Default: the latest time in the data."
    },
    {
     "name": "bucket",
     "title": "Time bucket",
     "type": "Choice",
     "description": "Length of each time slot the occupancy is measured in."
    },
    {
     "name": "capacity",
     "title": "Capacity",
     "type": "Number",
     "description": "0 = none."
    },
    {
     "name": "capacity_by_group",
     "title": "Capacity by group",
     "type": "Text",
     "description": "For example: Ward A=24, Ward B=18"
    }
   ],
   "outputs": [
    {
     "id": "occupancy",
     "label": "Occupancy",
     "description": "Per group and bucket: average and peak occupancy, arrivals, departures, capacity, utilisation, over capacity."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per group: intervals, period, peak and when, average occupancy and utilisation, hours over capacity, median duration."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks, occupancy over time takes expanding each interval into time buckets and aggregating; this block calculates exact average and peak occupancy per bucket in one step.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Date-Range-Expander",
     "name": "Date Range Expander",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Date-Range-Expander/index.html",
     "note": "Expands each start / end range into one row per day, week or month, with days per period."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "strata",
     "name": "Strata",
     "page": "https://visokio.github.io/omniscope-custom-views/strata/index.html",
     "note": "Draws the intervals themselves as a timeline, where the stack's height shows how many are open at each moment."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Occupancy-From-Intervals/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Occupancy-From-Intervals",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Occupancy-From-Intervals/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Occupancy-From-Intervals/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Occupancy-From-Intervals/sample-data/ward-stays.csv"
    ]
   }
  },
  {
   "id": "Analytics/Path-Analysis",
   "name": "Path Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "The most common event sequences per user or session, step-numbered transitions for Sankey charts, and next-step probabilities.",
   "tags": [
    "path analysis",
    "user journeys",
    "clickstream",
    "Sankey",
    "Markov",
    "process mining",
    "sequences",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "path_field",
     "title": "User or session field",
     "type": "Field",
     "description": "Identifies one path: all events with the same value form a sequence."
    },
    {
     "name": "event_field",
     "title": "Event field",
     "type": "Field",
     "description": "The page, screen or step recorded in each event."
    },
    {
     "name": "time_field",
     "title": "Time field (optional)",
     "type": "Field",
     "description": "Orders the events; without it, input order is used."
    }
   ],
   "options": [
    {
     "name": "max_steps",
     "title": "Maximum steps",
     "type": "Whole number",
     "description": "Paths are cut after this many steps, so long journeys group together."
    },
    {
     "name": "top_n",
     "title": "Top paths",
     "type": "Whole number",
     "description": "0 = all."
    },
    {
     "name": "collapse_repeats",
     "title": "Collapse repeated consecutive events",
     "type": "Yes / no",
     "description": "Treats the same event several times in a row (such as page reloads) as one step."
    },
    {
     "name": "start_event",
     "title": "Start at event (optional)",
     "type": "Text",
     "description": "Paths start at its first occurrence; paths without it are left out."
    },
    {
     "name": "end_events",
     "title": "End events (optional)",
     "type": "Text",
     "description": "Comma-separated; a path stops at the first one, for example: purchase"
    }
   ],
   "outputs": [
    {
     "id": "paths",
     "label": "Paths",
     "description": "Top paths: rank, path, steps, count, share, cumulative share, first / last event, reached end event, median minutes."
    },
    {
     "id": "transitions",
     "label": "Transitions",
     "description": "Step-numbered From / To links with counts, for Sankey charts."
    },
    {
     "id": "next_step",
     "label": "Next step",
     "description": "From event, to event (or exit), transitions and probability."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Network Analysis** block analyses links between nodes, not ordered paths per user. This block finds the most common event sequences.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Funnel-Analysis",
     "name": "Funnel Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Funnel-Analysis/index.html",
     "note": "Conversion and drop-off through a fixed list of steps."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Case-Bottlenecks",
     "name": "Case Duration and Bottlenecks",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Case-Bottlenecks/index.html",
     "note": "Durations, rework and slow handovers from a process event log."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Sessionise-Events",
     "name": "Sessionise Events",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Sessionise-Events/index.html",
     "note": "Splits each user's events into sessions, so paths can be measured per session."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "processmap",
     "name": "Process Map",
     "page": "https://visokio.github.io/omniscope-custom-views/processmap/index.html",
     "note": "Maps how cases move between steps from the same kind of event log, with rework and variants."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Sankey",
     "page": null,
     "note": "Draws the Transitions output (step-numbered From and To links) as flows."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Path-Analysis/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Path-Analysis",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Path-Analysis/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Path-Analysis/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Path-Analysis/sample-data/web-sessions.csv"
    ]
   }
  },
  {
   "id": "Analytics/Period-Comparison",
   "name": "Period Comparison",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Builds the standard month / QTD / YTD / moving annual total comparison versus prior periods per group, with a fiscal year start of your choice, plus a monthly trend table.",
   "tags": [
    "ytd",
    "mat",
    "year on year",
    "variance",
    "finance",
    "reporting",
    "fiscal year",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each value."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "Sales, revenue, units, cost..."
    },
    {
     "name": "group_fields",
     "title": "Group by (optional)",
     "type": "Fields",
     "description": "One comparison per value, such as Region."
    }
   ],
   "options": [
    {
     "name": "as_of",
     "title": "Reporting date (optional)",
     "type": "Text",
     "description": "Any date in the reporting month, such as 2025-09-30. Empty = latest complete month in the data."
    },
    {
     "name": "fiscal_year_start",
     "title": "Fiscal year starts in month",
     "type": "Whole number",
     "description": "1 = January, 4 = April."
    },
    {
     "name": "complete_months_only",
     "title": "Use the latest complete month",
     "type": "Yes / no",
     "description": "When no reporting date is set and the last month in the data is partial, report the month before."
    }
   ],
   "outputs": [
    {
     "id": "comparison",
     "label": "Comparison",
     "description": "Per group (and total): month, prior month, same month last year, QTD, YTD, MAT and their comparators, with changes."
    },
    {
     "id": "trend",
     "label": "Monthly trend",
     "description": "Per group and month: value, same month last year, % change, YTD, MAT, fiscal year."
    }
   ],
   "libraries": [],
   "relatedSummary": "The standard month, quarter-to-date, year-to-date and moving annual total comparison is possible with date formulas and several Aggregate blocks; this block builds it in one step.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Variance-Bridge",
     "name": "Variance Bridge",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Variance-Bridge/index.html",
     "note": "Explains the gap between two totals as a waterfall of category variances."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "yearonyear",
     "name": "Year on Year",
     "page": "https://visokio.github.io/omniscope-custom-views/yearonyear/index.html",
     "note": "Draws this year against last year by month, with a like-for-like year-to-date change and financial years."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Period-Comparison/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Period-Comparison",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Period-Comparison/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Period-Comparison/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Period-Comparison/sample-data/daily-sales.csv"
    ]
   }
  },
  {
   "id": "Analytics/Point-In-Polygon",
   "name": "Point in Polygon",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Geo",
   "description": "Finds which polygon (territory, zone, area) each latitude / longitude point falls in, from GeoJSON or WKT shapes, and counts points per polygon.",
   "tags": [
    "point in polygon",
    "spatial join",
    "geojson",
    "wkt",
    "territory",
    "geo",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "lat_field",
     "title": "Latitude",
     "type": "Field",
     "description": "Latitude of each point, in decimal degrees."
    },
    {
     "name": "lon_field",
     "title": "Longitude",
     "type": "Field",
     "description": "Longitude of each point, in decimal degrees."
    },
    {
     "name": "value_field",
     "title": "Value to sum per polygon (optional)",
     "type": "Field",
     "description": "A number to total per polygon, such as spend."
    },
    {
     "name": "geometry_field",
     "title": "Geometry (GeoJSON or WKT)",
     "type": "Field",
     "description": "The shape of each polygon in input 2, as GeoJSON or WKT text."
    }
   ],
   "options": [
    {
     "name": "match_mode",
     "title": "Points in overlapping polygons",
     "type": "Choice",
     "description": "Where polygons overlap: keep only the first match, or a row for each."
    },
    {
     "name": "keep_unmatched",
     "title": "Keep points outside all polygons",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "points",
     "label": "Points",
     "description": "Input 1 with the containing polygon's fields, Polygon Row, Polygons Containing Point and Match Status."
    },
    {
     "id": "polygons",
     "label": "Polygons",
     "description": "Input 2 (without geometry) with the number of points (and value) in each polygon and a geometry status."
    }
   ],
   "libraries": [],
   "relatedSummary": "Use the community **Shapefile** block to read a shapefile directly, and this block when you also want a count of points per polygon, or every polygon a point falls in where regions overlap.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Geo/Shapefile",
     "name": "Shapefile",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Shapefile/index.html",
     "note": "Joins latitude / longitude points to regions from a shapefile, a GeoJSON file or shapes on a second input. This block also counts points per polygon and can return every overlapping match."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Geo/Centroids",
     "name": "Centroids from GeoJSON",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Centroids/index.html",
     "note": "Centre points of the shapes, from GeoJSON or WKT text or a boundary file."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-In-Polygon/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Point-In-Polygon",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Point-In-Polygon/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Point-In-Polygon/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Point-In-Polygon/sample-data/customers.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Point-In-Polygon/sample-data/sales-territories.csv"
    ]
   }
  },
  {
   "id": "Analytics/Point-To-Point-Distance",
   "name": "Point-to-Point Distance",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Geo",
   "description": "Great-circle distance, bearing, direction and midpoint between two latitude / longitude pairs per row, with optional road distance and travel time estimates.",
   "tags": [
    "distance",
    "haversine",
    "bearing",
    "geo",
    "journeys",
    "logistics",
    "travel time",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "from_lat",
     "title": "From latitude",
     "type": "Field",
     "description": "Latitude of the starting point, in decimal degrees."
    },
    {
     "name": "from_lon",
     "title": "From longitude",
     "type": "Field",
     "description": "Longitude of the starting point, in decimal degrees."
    },
    {
     "name": "to_lat",
     "title": "To latitude",
     "type": "Field",
     "description": "Latitude of the destination, in decimal degrees."
    },
    {
     "name": "to_lon",
     "title": "To longitude",
     "type": "Field",
     "description": "Longitude of the destination, in decimal degrees."
    }
   ],
   "options": [
    {
     "name": "unit",
     "title": "Unit",
     "type": "Choice",
     "description": "Unit for distances; speed is then km/h or mph."
    },
    {
     "name": "road_factor",
     "title": "Road distance factor (optional)",
     "type": "Number",
     "description": "For example 1.3; 0 = none."
    },
    {
     "name": "speed",
     "title": "Average speed for travel time (optional)",
     "type": "Number",
     "description": "In km/h or mph; needs a road factor."
    }
   ],
   "outputs": [
    {
     "id": "distances",
     "label": "Distances",
     "description": "The input with distance, bearing, direction, midpoint, optional road distance and minutes, valid flag."
    }
   ],
   "libraries": [],
   "relatedSummary": "The LAT_LON_DISTANCE formula gives the distance between two points. This block adds bearing, compass direction and midpoint, in kilometres or miles, with optional road distance and travel time estimates.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Find-Nearest",
     "name": "Find Nearest",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Find-Nearest/index.html",
     "note": "Finds the nearest locations in another table for each point."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Trade-Area-Summary",
     "name": "Trade Area Summary",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Trade-Area-Summary/index.html",
     "note": "Counts points around each site by radius."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "globeflows",
     "name": "Globe Flows",
     "page": "https://visokio.github.io/omniscope-custom-views/globeflows/index.html",
     "note": "Draws the same origin and destination pairs as arcs on a 3D globe."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-To-Point-Distance/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Point-To-Point-Distance",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Point-To-Point-Distance/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Point-To-Point-Distance/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Point-To-Point-Distance/sample-data/deliveries.csv"
    ]
   }
  },
  {
   "id": "Analytics/Prediction/KNN",
   "name": "K-Nearest-Neighbours",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Prediction",
   "description": "Predicts a category for each record from the k most similar records whose category is known (k-nearest neighbours), on numeric fields standardised first, with the share of neighbours that agree.",
   "tags": [
    "knn",
    "nearest",
    "neighbours",
    "prediction",
    "classification",
    "machine learning"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "fieldToPredict",
     "title": "Field to predict",
     "type": "Field",
     "description": "The Text field holding the category to predict, in the first input. Records where it is empty are not used to learn from; they are predicted."
    },
    {
     "name": "fieldsToUse",
     "title": "Fields to predict from",
     "type": "Fields",
     "description": "The numeric fields that measure how similar two records are. Used when \"Use all numeric fields\" is unticked. A second input must have fields with the same names."
    }
   ],
   "options": [
    {
     "name": "useAllNumericFields",
     "title": "Use all numeric fields",
     "type": "Yes / no",
     "description": "Predict from every Integer and Decimal field in the first input. Untick to choose the fields in \"Fields to predict from\"."
    },
    {
     "name": "numberOfNeighbours",
     "title": "Number of neighbours",
     "type": "Whole number",
     "description": "The k in k-nearest neighbours: how many of the most similar known records vote on each prediction. Odd numbers avoid some ties."
    },
    {
     "name": "standardise",
     "title": "Standardise fields",
     "type": "Yes / no",
     "description": "Rescale each field to mean 0 and standard deviation 1 (from the known records) before measuring similarity, so a field measured in thousands does not outweigh one measured in single figures."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Predicted",
     "description": "The second input (or, with one input, the first) with Predicted (field) and Vote share; both empty where a field to predict from is missing."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Prediction** block is the general choice, for many fields, mixed field types or large data. This block fits no model, which suits a few numeric measures where \"records like this one\" is the right logic, and re-predicts the labelled records, a ready-made check for **Model Validation**.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Prediction",
     "page": null,
     "note": "Builds a model for a target field from chosen predictors, with likelihood fields for each value. This block classifies from the nearest known records, with no model to fit, and gives the share of neighbours that agree."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Prediction/SVM",
     "name": "Support Vector Machine",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/SVM/index.html",
     "note": "Learns boundaries between categories from training records. KNN needs no training step and reports how many neighbours agree; SVM gives no confidence."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Validation/Model-Validation",
     "name": "Model Validation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Validation/Model-Validation/index.html",
     "note": "Confusion matrix, precision, recall and F1 for this block's predictions on held-back records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Regression",
     "name": "Regression",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Regression/index.html",
     "note": "Logistic regression for yes / no outcomes, with coefficients and probabilities."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/KNN/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Prediction/KNN",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/KNN/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Prediction/KNN/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/KNN/sample-data/known-customers.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/KNN/sample-data/new-customers.csv"
    ]
   }
  },
  {
   "id": "Analytics/Prediction/SVM",
   "name": "Support Vector Machine",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Prediction",
   "description": "Predicts a category (such as Normal, Worn or Faulty) for new records from numeric fields, using a support vector machine trained on records whose category is known.",
   "tags": [
    "svm",
    "support vector machine",
    "classification",
    "prediction",
    "machine learning",
    "scikit-learn"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fieldToPredict",
     "title": "Field to predict",
     "type": "Field",
     "description": "The category to learn from the first input and predict, for example a status or a segment."
    },
    {
     "name": "fieldsToUse",
     "title": "Fields to predict from",
     "type": "Fields",
     "description": "The numeric fields the prediction is based on. Used when \"Use all numeric fields\" is unticked."
    }
   ],
   "options": [
    {
     "name": "useAllNumericFields",
     "title": "Use all numeric fields",
     "type": "Yes / no",
     "description": "Predict from every Integer and Decimal field except the Field to predict. Untick to choose the fields in \"Fields to predict from\"."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Predicted",
     "description": "The records to predict (the second input, or the first if nothing is connected there) with a Prediction field."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Prediction** block is the general choice, with a confidence per prediction and support for text fields. This block is a single, well-known method on numeric fields with its settings in the script to change; it gives no confidence, so use **Regression** instead when the prediction must be explained.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Prediction",
     "page": null,
     "note": "Builds a model for a target field from chosen predictors, with likelihood fields for each value. This block trains a support vector machine on numeric fields and predicts a second input."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Prediction/KNN",
     "name": "K-Nearest-Neighbours",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/KNN/index.html",
     "note": "Classifies from the k most similar known records and gives the share that agree. Try both on the same split and compare."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Validation/Model-Validation",
     "name": "Model Validation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Validation/Model-Validation/index.html",
     "note": "Checks this block's predictions against known answers: confusion matrix, precision, recall and F1."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Regression",
     "name": "Regression",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Regression/index.html",
     "note": "Logistic regression for yes / no outcomes, with coefficients and probabilities."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/SVM/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Prediction/SVM",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/SVM/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Prediction/SVM/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/SVM/sample-data/pump-history.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/SVM/sample-data/pump-readings-inspected.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Prediction/SVM/sample-data/pump-readings.csv"
    ]
   }
  },
  {
   "id": "Analytics/Price-Elasticity",
   "name": "Price Elasticity",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Pricing",
   "description": "Estimates price elasticity of demand per product with a log-log regression (optional control drivers), with confidence intervals, revenue impact and demand curves.",
   "tags": [
    "price elasticity",
    "pricing",
    "demand",
    "regression",
    "revenue management",
    "retail",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "price_field",
     "title": "Price field",
     "type": "Field",
     "description": "Price for each observation, such as the average price in a week."
    },
    {
     "name": "volume_field",
     "title": "Volume field",
     "type": "Field",
     "description": "Units sold (or another quantity)."
    },
    {
     "name": "group_fields",
     "title": "Estimate separately for each",
     "type": "Fields",
     "description": "For example Product, or Product and Region."
    },
    {
     "name": "control_fields",
     "title": "Control drivers (optional)",
     "type": "Fields",
     "description": "Numeric drivers to hold constant, such as a 0 / 1 promotion flag."
    }
   ],
   "options": [
    {
     "name": "confidence",
     "title": "Confidence level",
     "type": "Number",
     "description": "For the confidence interval on each elasticity."
    },
    {
     "name": "price_change_pct",
     "title": "Price change to evaluate (%)",
     "type": "Number",
     "description": "Used for the volume and revenue effect columns."
    },
    {
     "name": "min_observations",
     "title": "Minimum observations",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "elasticities",
     "label": "Elasticities",
     "description": "Per product: elasticity, confidence interval, p-value, R squared, price range, volume and revenue effect, interpretation."
    },
    {
     "id": "curves",
     "label": "Demand curves",
     "description": "Per product: predicted volume and revenue at 25 prices across the observed range."
    },
    {
     "id": "observations",
     "label": "Observations",
     "description": "The input with Fitted Volume and Used In Model."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Regression** block fits regressions in general; this block applies the log-log elasticity model per product, with controls and a plain-language reading of the result.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Regression",
     "name": "Regression",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Regression/index.html",
     "note": "Linear or logistic regression on any fields, with the full statistics report."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Elasticity/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Price-Elasticity",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Elasticity/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Price-Elasticity/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Elasticity/sample-data/weekly-price-volume.csv"
    ]
   }
  },
  {
   "id": "Analytics/Price-Index",
   "name": "Price Index",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Laspeyres, Paasche and Fisher price indices, value and volume indices from item prices and quantities per period, per group.",
   "tags": [
    "price index",
    "Laspeyres",
    "Paasche",
    "Fisher",
    "cost index",
    "inflation",
    "procurement",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "Identifies an item whose price is tracked."
    },
    {
     "name": "period_field",
     "title": "Period field",
     "type": "Field",
     "description": "The period of each price and quantity, such as Year."
    },
    {
     "name": "price_field",
     "title": "Unit price field",
     "type": "Field",
     "description": "Price paid per unit."
    },
    {
     "name": "quantity_field",
     "title": "Quantity field",
     "type": "Field",
     "description": "Units bought, used to weight the items."
    },
    {
     "name": "group_field",
     "title": "One index per (optional)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "base_period",
     "title": "Base period",
     "type": "Text",
     "description": "Default: the first period."
    }
   ],
   "outputs": [
    {
     "id": "index",
     "label": "Index",
     "description": "Per group and period: items compared and missing, Laspeyres, Paasche, Fisher, value and volume indices, change."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Inflation-Adjustment",
     "name": "Inflation Adjustment",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Inflation-Adjustment/index.html",
     "note": "Restates money values in real terms using an index table, such as this block's output."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Price-Volume-Mix",
     "name": "Price-Volume-Mix Bridge",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Volume-Mix/index.html",
     "note": "Splits a revenue change into price, volume and mix effects per item."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Index/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Price-Index",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Index/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Price-Index/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Index/sample-data/purchases.csv"
    ]
   }
  },
  {
   "id": "Analytics/Price-Volume-Mix",
   "name": "Price-Volume-Mix Bridge",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Decomposes revenue change between two periods into volume, mix, price, new and lost product effects that add up exactly, per item and optional group.",
   "tags": [
    "price volume mix",
    "pvm",
    "variance analysis",
    "revenue bridge",
    "waterfall",
    "finance",
    "commercial",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "period_field",
     "title": "Period field",
     "type": "Field",
     "description": "For example Year, or a field with values such as FY24 / FY25."
    },
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "The level at which price and mix are measured, such as Product or SKU."
    },
    {
     "name": "volume_field",
     "title": "Volume field",
     "type": "Field",
     "description": "Units, kilograms, contracts..."
    },
    {
     "name": "revenue_field",
     "title": "Revenue field",
     "type": "Field",
     "description": "Revenue for the item in the period; price is revenue divided by volume."
    },
    {
     "name": "group_fields",
     "title": "Bridge separately for each (optional)",
     "type": "Fields",
     "description": "For example Region."
    }
   ],
   "options": [
    {
     "name": "base_period",
     "title": "Base period value",
     "type": "Text",
     "description": "Empty = the first value of the period field."
    },
    {
     "name": "current_period",
     "title": "Current period value",
     "type": "Text",
     "description": "Empty = the last value of the period field."
    }
   ],
   "outputs": [
    {
     "id": "bridge",
     "label": "Bridge",
     "description": "Per (group and) step: base revenue, volume, mix, price, new, lost, current revenue, with bar start / end for waterfall charts."
    },
    {
     "id": "items",
     "label": "Item effects",
     "description": "Per (group and) item: status, volumes, revenues, prices, volume / mix / price / new-lost effects."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "pvmbridge",
     "name": "Price Volume Mix",
     "page": "https://visokio.github.io/omniscope-custom-views/pvmbridge/index.html",
     "note": "Calculates the same volume, mix, price, new and discontinued effects and draws the waterfall in a report, recalculated for any filter. Use this block for one bridge per group, or to keep each item's effects as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Variance-Bridge",
     "name": "Variance Bridge",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Variance-Bridge/index.html",
     "note": "Bridges any two totals by category, without the price and volume split."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Price-Index",
     "name": "Price Index",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Index/index.html",
     "note": "Price and volume indices for a whole basket of items, per period."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Waterfall",
     "page": null,
     "note": "Draws the Bridge output as a waterfall chart."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Volume-Mix/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Price-Volume-Mix",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Volume-Mix/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Price-Volume-Mix/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Price-Volume-Mix/sample-data/product-sales-2024-2025.csv"
    ]
   }
  },
  {
   "id": "Analytics/Principal-Components",
   "name": "Principal Component Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "PCA on numeric fields: explained variance, loadings and correlations, automatic or fixed number of components, and scores per row.",
   "tags": [
    "PCA",
    "principal components",
    "dimension reduction",
    "index",
    "segmentation",
    "factor analysis",
    "statistics",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Numeric fields",
     "type": "Fields",
     "description": "Leave empty to use every numeric field."
    }
   ],
   "options": [
    {
     "name": "standardise",
     "title": "Standardise fields (correlation PCA)",
     "type": "Yes / no",
     "description": "Scales each field to unit variance first, so fields in large units do not dominate. Untick for covariance PCA."
    },
    {
     "name": "components",
     "title": "Components to keep",
     "type": "Whole number",
     "description": "0 = automatic."
    },
    {
     "name": "variance_target",
     "title": "Variance to explain (covariance PCA, automatic)",
     "type": "Number"
    }
   ],
   "outputs": [
    {
     "id": "components",
     "label": "Components",
     "description": "Per component: eigenvalue, share and cumulative share of variance, kept."
    },
    {
     "id": "loadings",
     "label": "Loadings",
     "description": "Per kept component and field: loading, correlation with the component, rank, main fields."
    },
    {
     "id": "scores",
     "label": "Scores",
     "description": "The input with a score per kept component and the unexplained distance."
    }
   ],
   "libraries": [],
   "relatedSummary": "Use the core **Reduce Dimensions** block when you want the reduced fields for modelling or charts. This block reports the analysis itself, to understand what drives the variation.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Reduce Dimensions",
     "page": null,
     "note": "Compresses numeric fields into fewer dimensions using PCA (or LDA). This block reports explained variance and loadings as tables."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "scatter3d",
     "name": "3D Scatter",
     "page": "https://visokio.github.io/omniscope-custom-views/scatter3d/index.html",
     "note": "Plots three component scores in 3D, coloured by a category."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Principal-Components/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Principal-Components",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Principal-Components/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Principal-Components/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Principal-Components/sample-data/area-profiles.csv"
    ]
   }
  },
  {
   "id": "Analytics/Product-Recommendations",
   "name": "Product Recommendations",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Retail",
   "description": "Item-to-item recommendations from purchase history: top new products per customer with the reason, and similar products per product.",
   "tags": [
    "recommendations",
    "collaborative filtering",
    "cross-sell",
    "next best product",
    "personalisation",
    "retail",
    "e-commerce",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "customer_field",
     "title": "Customer field",
     "type": "Field"
    },
    {
     "name": "product_field",
     "title": "Product field",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "top_n",
     "title": "Recommendations per customer",
     "type": "Whole number"
    },
    {
     "name": "similar_per_product",
     "title": "Similar products per product",
     "type": "Whole number",
     "description": "0 = none."
    },
    {
     "name": "min_customers",
     "title": "Minimum customers per product",
     "type": "Whole number"
    },
    {
     "name": "shrinkage",
     "title": "Shrinkage",
     "type": "Number",
     "description": "0 = none; 10 down-weights pairs bought together by few customers."
    },
    {
     "name": "exclude_bought",
     "title": "Leave out products already bought",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "recommendations",
     "label": "Recommendations",
     "description": "Per customer: rank, recommended product, score, because you bought, similarity, products bought."
    },
    {
     "id": "similar",
     "label": "Similar",
     "description": "Per product: similar products with similarity and customers who bought both."
    },
    {
     "id": "coverage",
     "label": "Coverage",
     "description": "Customers; products in the data, split into products scored and products left out; products recommended at least once."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Basket Analysis** block works across all transactions; this block turns purchase history into recommendations for each customer: the top products they have not bought yet, with the reason.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Basket Analysis",
     "page": null,
     "note": "Association rules for items bought together."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Basket-Affinity",
     "name": "Basket Affinity Matrix",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Basket-Affinity/index.html",
     "note": "Support, confidence, lift and Jaccard for every pair of items, as a matrix."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "marketbasket",
     "name": "Market Basket",
     "page": "https://visokio.github.io/omniscope-custom-views/marketbasket/index.html",
     "note": "Shows which items are bought together, as a matrix with ranked rules."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Product-Recommendations/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Product-Recommendations",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Product-Recommendations/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Product-Recommendations/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Product-Recommendations/sample-data/purchases.csv"
    ]
   }
  },
  {
   "id": "Analytics/RFM-Segmentation",
   "name": "RFM Segmentation",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Marketing",
   "description": "Scores customers on recency, frequency and monetary value from transactions and assigns named segments such as Champions, At Risk and Lost, with a segment summary.",
   "tags": [
    "rfm",
    "segmentation",
    "customers",
    "crm",
    "marketing",
    "retention",
    "churn",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "customer_field",
     "title": "Customer field",
     "type": "Field",
     "description": "Identifies a customer."
    },
    {
     "name": "date_field",
     "title": "Order date field",
     "type": "Field",
     "description": "Date of each order, for recency."
    },
    {
     "name": "amount_field",
     "title": "Order value field",
     "type": "Field",
     "description": "Value of each order, summed for the monetary score."
    },
    {
     "name": "order_field",
     "title": "Order ID field (optional)",
     "type": "Field",
     "description": "Frequency counts distinct order IDs. Leave empty to count rows (one row per order)."
    }
   ],
   "options": [
    {
     "name": "analysis_date",
     "title": "Analysis date",
     "type": "Text",
     "description": "Recency is measured to this date, such as 2025-12-31. Leave empty for the day after the latest transaction."
    },
    {
     "name": "bins",
     "title": "Score bands",
     "type": "Whole number",
     "description": "5 gives scores 1 to 5 (quintiles)."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April."
    }
   ],
   "outputs": [
    {
     "id": "customers",
     "label": "Customers",
     "description": "One row per customer: recency, frequency, monetary, average order value, R / F / M scores, RFM score, segment."
    },
    {
     "id": "segments",
     "label": "Segments",
     "description": "One row per segment: customers, revenue, shares, averages, description, suggested action."
    }
   ],
   "libraries": [],
   "relatedSummary": "RFM scoring is possible with several Aggregate blocks, tiles and formulas; this block scores recency, frequency and value and names the segments in one step.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "rfmsegments",
     "name": "RFM Segments",
     "page": "https://visokio.github.io/omniscope-custom-views/rfmsegments/index.html",
     "note": "Scores customers 1 to 5 from the same transactions and shows named segments on the recency grid in a report, selecting a segment's customers; its eleven segments differ slightly from this block's ten. Use this block to keep each customer's scores and segment as fields, for campaigns or exports."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Customer-Lifetime-Value",
     "name": "Customer Lifetime Value",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Customer-Lifetime-Value/index.html",
     "note": "Predicted lifetime value and probability active from the same order history."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/RFM-Segmentation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/RFM-Segmentation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/RFM-Segmentation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/RFM-Segmentation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/RFM-Segmentation/sample-data/online-orders.csv"
    ]
   }
  },
  {
   "id": "Analytics/Reconciliation-Matcher",
   "name": "Reconciliation Matcher",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Matches two ledgers one-to-one on amount, reference and date with tolerances, plus group matches, and lists the unmatched items on each side.",
   "tags": [
    "reconciliation",
    "bank reconciliation",
    "matching",
    "accounts payable",
    "accounts receivable",
    "cash",
    "audit",
    "finance",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "amount_field_a",
     "title": "Amount",
     "type": "Field",
     "description": "Amount of each item in input 1."
    },
    {
     "name": "date_field_a",
     "title": "Date (optional)",
     "type": "Field",
     "description": "Date of each item in input 1, for the date window."
    },
    {
     "name": "reference_field_a",
     "title": "Reference (optional)",
     "type": "Field",
     "description": "Reference or narrative in input 1, compared with input 2's."
    },
    {
     "name": "amount_field_b",
     "title": "Amount",
     "type": "Field",
     "description": "Amount of each item in input 2."
    },
    {
     "name": "date_field_b",
     "title": "Date (optional)",
     "type": "Field",
     "description": "Date of each item in input 2."
    },
    {
     "name": "reference_field_b",
     "title": "Reference (optional)",
     "type": "Field",
     "description": "Reference in input 2."
    },
    {
     "name": "match_fields",
     "title": "Fields that must agree (optional)",
     "type": "Fields",
     "description": "Same field names in both inputs, such as Account or Currency."
    }
   ],
   "options": [
    {
     "name": "tolerance",
     "title": "Amount tolerance",
     "type": "Number",
     "description": "For example 0.01 for rounding differences."
    },
    {
     "name": "date_window",
     "title": "Date window (days)",
     "type": "Number",
     "description": "Maximum days between matched items."
    },
    {
     "name": "sign",
     "title": "Input 2 amounts",
     "type": "Choice",
     "description": "Choose Opposite when one side records as negative what the other records as positive."
    },
    {
     "name": "group_matching",
     "title": "Match groups with the same reference",
     "type": "Yes / no",
     "description": "One item against several that add up to it."
    }
   ],
   "outputs": [
    {
     "id": "side1",
     "label": "Items 1",
     "description": "Input 1 with Match Status, Match ID and Match Rule."
    },
    {
     "id": "side2",
     "label": "Items 2",
     "description": "Input 2 with Match Status, Match ID and Match Rule."
    },
    {
     "id": "matches",
     "label": "Matches",
     "description": "One row per match: rule, items and amount on each side, difference, date difference, references."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Matched and unmatched items and amounts per side, and matches per rule."
    }
   ],
   "libraries": [],
   "relatedSummary": "A core **Join** matches on exact keys. This block matches two ledgers one-to-one on amount, with tolerances, references and date windows, including one-to-many groups, and lists what is left unmatched.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Compare",
     "name": "Data Compare",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Compare/index.html",
     "note": "Compares two versions of one dataset by key and lists added, removed and changed records."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Reconciliation-Matcher/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Reconciliation-Matcher",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Reconciliation-Matcher/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Reconciliation-Matcher/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Reconciliation-Matcher/sample-data/bank-statement.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Reconciliation-Matcher/sample-data/cash-book.csv"
    ]
   }
  },
  {
   "id": "Analytics/Regression",
   "name": "Regression",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Linear or logistic regression with coefficients, standard errors, p-values, confidence intervals, odds ratios, VIF, fit statistics and scored rows.",
   "tags": [
    "regression",
    "linear regression",
    "logistic regression",
    "p-value",
    "odds ratio",
    "VIF",
    "AUC",
    "statistics",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "target_field",
     "title": "Target field",
     "type": "Field",
     "description": "The value to explain: a number (linear) or a yes / no outcome (logistic)."
    },
    {
     "name": "predictor_fields",
     "title": "Predictor fields",
     "type": "Fields",
     "description": "Numbers or text (text is dummy-coded)."
    }
   ],
   "options": [
    {
     "name": "model",
     "title": "Model",
     "type": "Choice",
     "description": "Automatic chooses logistic for a two-value target and linear otherwise."
    },
    {
     "name": "positive_class",
     "title": "Positive class (logistic)",
     "type": "Text",
     "description": "Default: Yes / 1 / True if present."
    },
    {
     "name": "confidence",
     "title": "Confidence level",
     "type": "Number",
     "description": "For the coefficient confidence intervals."
    },
    {
     "name": "threshold",
     "title": "Classification threshold (logistic)",
     "type": "Number"
    },
    {
     "name": "max_levels",
     "title": "Most values for a text predictor",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "coefficients",
     "label": "Coefficients",
     "description": "Per term: coefficient, standard error, t or z, p-value, confidence interval, VIF, odds ratio (logistic), significant."
    },
    {
     "id": "fit",
     "label": "Fit",
     "description": "Model statistics: rows, R-squared, F test, AIC; or log-likelihood, pseudo R-squared, LR test, AUC, accuracy."
    },
    {
     "id": "scored",
     "label": "Scored",
     "description": "The input with prediction, residual and interval, or probability and predicted class."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Regression** block suits scoring; this block is for explaining, with odds ratios, collinearity checks and fit measures for linear and logistic models. To predict categories, see the core **Prediction** block.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Regression",
     "page": null,
     "note": "Estimates relationships between a response and predictor fields and generates predictions. This block reports coefficients with standard errors, p-values and confidence intervals, and fits logistic models."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Validation/Model-Validation",
     "name": "Model Validation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Validation/Model-Validation/index.html",
     "note": "Confusion matrix and validation statistics for any classifier's predictions."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Prediction/KNN",
     "name": "K-Nearest-Neighbours",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/KNN/index.html",
     "note": "Predicts a category from the most similar known records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Prediction/SVM",
     "name": "Support Vector Machine",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/SVM/index.html",
     "note": "A support vector machine trained on known records, to predict a category."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Price-Elasticity",
     "name": "Price Elasticity",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Elasticity/index.html",
     "note": "A log-log regression per product, set up for price and demand."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "modelevaluation",
     "name": "Model Evaluation",
     "page": "https://visokio.github.io/omniscope-custom-views/modelevaluation/index.html",
     "note": "Evaluates a logistic model's scored rows (actual outcome and probability) with ROC, gains and a movable threshold."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Regression/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Regression",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Regression/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Regression/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Regression/sample-data/telecom-customers.csv"
    ]
   }
  },
  {
   "id": "Analytics/Running-Balance",
   "name": "Running Balance",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Running balances per account from opening balances and dated transactions, with lowest balance, days below zero or a limit, and average daily balance.",
   "tags": [
    "running balance",
    "ledger",
    "overdraft",
    "credit control",
    "cash",
    "average daily balance",
    "accounts",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "account_field",
     "title": "Account",
     "type": "Field",
     "description": "Identifies the account each transaction belongs to."
    },
    {
     "name": "date_field",
     "title": "Date",
     "type": "Field",
     "description": "Date of each transaction."
    },
    {
     "name": "amount_field",
     "title": "Amount (in positive, out negative)",
     "type": "Field",
     "description": "Money in as positive, money out as negative."
    },
    {
     "name": "opening_account_field",
     "title": "Account",
     "type": "Field",
     "description": "Account in the opening balances table, matched to input 1."
    },
    {
     "name": "opening_balance_field",
     "title": "Opening balance",
     "type": "Field",
     "description": "Balance before the first transaction."
    }
   ],
   "options": [
    {
     "name": "limit",
     "title": "Limit (optional)",
     "type": "Number",
     "description": "For example 500 for an overdraft limit of 500; 0 = none."
    },
    {
     "name": "as_of",
     "title": "As of date",
     "type": "Text",
     "description": "Default: the latest transaction date."
    }
   ],
   "outputs": [
    {
     "id": "transactions",
     "label": "Transactions",
     "description": "The input with Balance After, Below Zero (and Below Limit)."
    },
    {
     "id": "accounts",
     "label": "Accounts",
     "description": "Per account: opening, in, out, closing, lowest balance and date, days below zero / limit, average daily balance."
    }
   ],
   "libraries": [],
   "relatedSummary": "The RUNNINGTOTAL formula gives a running total, but not per account from opening balances, with daily closing balances and overdraft figures; this block does.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Multi-Row-Formula",
     "name": "Multi-Row Formula",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Row-Formula/index.html",
     "note": "Running totals and other window calculations per group, without balance statistics."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Running-Balance/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Running-Balance",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Running-Balance/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Running-Balance/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Running-Balance/sample-data/opening-balances.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Running-Balance/sample-data/transactions.csv"
    ]
   }
  },
  {
   "id": "Analytics/Safety-Stock",
   "name": "Safety Stock and Reorder Point",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Supply Chain",
   "description": "Calculates safety stock, reorder points, order-up-to levels and EOQ per item from demand history and lead times for a target service level, with a reorder list.",
   "tags": [
    "safety stock",
    "reorder point",
    "inventory",
    "eoq",
    "service level",
    "supply chain",
    "replenishment",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "Identifies the item, such as a part number."
    },
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each demand record; demand is summed per period."
    },
    {
     "name": "quantity_field",
     "title": "Quantity field",
     "type": "Field",
     "description": "Quantity demanded or shipped."
    },
    {
     "name": "param_item_field",
     "title": "Item field",
     "type": "Field"
    },
    {
     "name": "param_lead_field",
     "title": "Lead time",
     "type": "Field"
    },
    {
     "name": "param_lead_sd_field",
     "title": "Lead time std dev",
     "type": "Field"
    },
    {
     "name": "param_cost_field",
     "title": "Unit cost",
     "type": "Field"
    },
    {
     "name": "param_stock_field",
     "title": "Stock on hand",
     "type": "Field"
    },
    {
     "name": "param_service_field",
     "title": "Service level override",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "period",
     "title": "Demand period",
     "type": "Choice",
     "description": "Lead times and review periods are in this unit."
    },
    {
     "name": "service_level",
     "title": "Target service level",
     "type": "Number",
     "description": "0.95 = 95% chance of no stock-out per replenishment cycle."
    },
    {
     "name": "lead_time",
     "title": "Lead time (periods)",
     "type": "Number",
     "description": "Used when input 2 gives none."
    },
    {
     "name": "review_period",
     "title": "Review period (periods)",
     "type": "Number",
     "description": "Time between stock reviews, for the order-up-to level."
    },
    {
     "name": "ordering_cost",
     "title": "Cost per order",
     "type": "Number",
     "description": "For EOQ. 0 = no EOQ."
    },
    {
     "name": "holding_rate",
     "title": "Annual holding cost rate",
     "type": "Number",
     "description": "Share of unit cost per year, such as 0.25."
    }
   ],
   "outputs": [
    {
     "id": "levels",
     "label": "Stock levels",
     "description": "Per item: demand statistics, lead time, service level, z, safety stock, reorder point, order-up-to level, EOQ, cover, status, suggested order."
    },
    {
     "id": "reorder",
     "label": "Reorder list",
     "description": "Items at or below their reorder point, least cover first, with the suggested order quantity."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Stock-Run-Out",
     "name": "Stock Run-Out Projection",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Stock-Run-Out/index.html",
     "note": "Projects stock day by day to find run-out dates and shortfalls."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/ABC-Classification",
     "name": "ABC Classification",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/ABC-Classification/index.html",
     "note": "ABC-XYZ classes, for setting service levels by item class."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Safety-Stock/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Safety-Stock",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Safety-Stock/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Safety-Stock/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Safety-Stock/sample-data/part-demand.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Safety-Stock/sample-data/part-parameters.csv"
    ]
   }
  },
  {
   "id": "Analytics/Sample-Size-Calculator",
   "name": "Sample Size Calculator",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Sample size and power for A/B tests on proportions or means and for survey margins of error, per scenario, with days needed and power curves.",
   "tags": [
    "sample size",
    "power analysis",
    "A/B testing",
    "experiment design",
    "survey",
    "margin of error",
    "statistics",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "test_type",
     "title": "Type",
     "type": "Choice"
    },
    {
     "name": "baseline",
     "title": "Baseline (rate or mean)",
     "type": "Number",
     "description": "For example 0.05 for a 5% conversion rate."
    },
    {
     "name": "effect",
     "title": "Smallest effect to detect",
     "type": "Number",
     "description": "Relative (0.1 = 10% lift) or absolute."
    },
    {
     "name": "relative_effect",
     "title": "Effect is relative to the baseline",
     "type": "Yes / no"
    },
    {
     "name": "sd",
     "title": "Standard deviation (means)",
     "type": "Number"
    },
    {
     "name": "margin",
     "title": "Margin of error (survey)",
     "type": "Number",
     "description": "For example 0.03 for plus or minus 3 points."
    },
    {
     "name": "population",
     "title": "Population size (survey, optional)",
     "type": "Number",
     "description": "0 = very large."
    },
    {
     "name": "daily_traffic",
     "title": "Daily traffic (optional)",
     "type": "Number",
     "description": "Visitors or customers per day across both groups."
    },
    {
     "name": "alpha",
     "title": "Significance level",
     "type": "Number",
     "description": "Chance of calling a difference real when it is not; 0.05 is usual."
    },
    {
     "name": "power",
     "title": "Power",
     "type": "Number",
     "description": "Chance of detecting the effect if it is there; 0.8 is usual."
    },
    {
     "name": "sides",
     "title": "Test",
     "type": "Choice",
     "description": "Two-sided detects a change in either direction; one-sided only the expected direction, and needs fewer records."
    },
    {
     "name": "ratio",
     "title": "Group 2 size relative to group 1",
     "type": "Number",
     "description": "1 = equal groups."
    }
   ],
   "outputs": [
    {
     "id": "sizes",
     "label": "Sample sizes",
     "description": "Per scenario: inputs, absolute and relative effect, sample size per group and total, days needed."
    },
    {
     "id": "power_curve",
     "label": "Power curve",
     "description": "Per two-group scenario: power at sample sizes from 10% to 200% of the required size."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/AB-Test",
     "name": "A/B Test",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/AB-Test/index.html",
     "note": "Analyses the test once it has run: lift, p-values and verdicts."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "abtest",
     "name": "A/B Test",
     "page": "https://visokio.github.io/omniscope-custom-views/abtest/index.html",
     "note": "Reads the test's results in a report once it has run, with the smallest change it could detect."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Sample-Size-Calculator/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Sample-Size-Calculator",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Sample-Size-Calculator/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Sample-Size-Calculator/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Sample-Size-Calculator/sample-data/test-plans.csv"
    ]
   }
  },
  {
   "id": "Analytics/Seasonal-Decomposition",
   "name": "Seasonal Decomposition",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Time Series",
   "description": "Splits time series into trend, seasonal and residual components (additive or multiplicative) with seasonally adjusted values, seasonal indices and strength measures.",
   "tags": [
    "seasonality",
    "decomposition",
    "trend",
    "seasonally adjusted",
    "time series",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each value."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "The series to decompose, such as monthly sales."
    },
    {
     "name": "group_fields",
     "title": "Series (optional)",
     "type": "Fields",
     "description": "One decomposition per combination of these fields, such as Category."
    }
   ],
   "options": [
    {
     "name": "frequency",
     "title": "Data frequency",
     "type": "Choice",
     "description": "Spacing of the data, which sets the default season length."
    },
    {
     "name": "model",
     "title": "Model",
     "type": "Choice",
     "description": "Multiplicative when the seasonal swing grows with the level; additive when it stays the same size."
    },
    {
     "name": "season_length",
     "title": "Season length (optional)",
     "type": "Whole number",
     "description": "Periods per cycle. 0 = automatic: 12 for monthly, 4 quarterly, 52 weekly, 7 daily."
    },
    {
     "name": "extend_trend",
     "title": "Extend the trend to the ends",
     "type": "Yes / no",
     "description": "Fill the first and last half-season of the trend with a straight-line continuation."
    }
   ],
   "outputs": [
    {
     "id": "components",
     "label": "Components",
     "description": "One row per series and period: value, trend, seasonal, residual, seasonally adjusted, interpolated flag."
    },
    {
     "id": "indices",
     "label": "Indices",
     "description": "One row per series and season: seasonal index and effect versus trend."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per series: seasonality and trend strength, peak and trough season, trend change, residual spread."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Time Series Forecast** block predicts future values; this block explains past ones, splitting each series into trend, seasonal and residual parts, so the two complement each other.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "decomposition",
     "name": "Seasonal Decomposition",
     "page": "https://visokio.github.io/omniscope-custom-views/decomposition/index.html",
     "note": "Decomposes one series in a report with a robust classical method and flags unusual periods. Use this block for many series at once, or to keep the seasonally adjusted values and seasonal indices as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Holt-Winters-Forecast",
     "name": "Holt-Winters Forecast",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Holt-Winters-Forecast/index.html",
     "note": "Forecasts the same series ahead, with trend and seasonality."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Time-Series-Anomalies",
     "name": "Time Series Anomaly Detection",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Time-Series-Anomalies/index.html",
     "note": "Flags single spikes and dips against a rolling baseline."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Seasonal-Decomposition/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Seasonal-Decomposition",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Seasonal-Decomposition/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Seasonal-Decomposition/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Seasonal-Decomposition/sample-data/monthly-category-sales.csv"
    ]
   }
  },
  {
   "id": "Analytics/Stock-Run-Out",
   "name": "Stock Run-Out Projection",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Supply Chain",
   "description": "Projects stock day by day from on-hand, demand and open purchase orders to give run-out dates, shortfalls and what to expedite.",
   "tags": [
    "stock projection",
    "run-out",
    "inventory",
    "purchase orders",
    "shortage",
    "days of cover",
    "supply chain",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "Identifies the item, such as a SKU."
    },
    {
     "name": "stock_field",
     "title": "Stock on hand",
     "type": "Field",
     "description": "Units on hand at the start date."
    },
    {
     "name": "safety_field",
     "title": "Safety stock (optional)",
     "type": "Field",
     "description": "Level to keep in reserve; days below it are flagged."
    },
    {
     "name": "daily_demand_field",
     "title": "Daily demand (optional)",
     "type": "Field",
     "description": "Steady demand per day, added to any dated demand."
    },
    {
     "name": "move_item_field",
     "title": "Item field",
     "type": "Field",
     "description": "Item of each movement, matched to input 1."
    },
    {
     "name": "move_date_field",
     "title": "Date",
     "type": "Field",
     "description": "Date of each demand or receipt."
    },
    {
     "name": "move_qty_field",
     "title": "Quantity",
     "type": "Field",
     "description": "Units in each movement."
    },
    {
     "name": "move_type_field",
     "title": "Type (optional)",
     "type": "Field",
     "description": "Receipts vs demand; without it all movements are demand."
    }
   ],
   "options": [
    {
     "name": "receipt_words",
     "title": "Words that mark a receipt",
     "type": "Text"
    },
    {
     "name": "start_date",
     "title": "Start date (optional)",
     "type": "Text",
     "description": "Default: the first movement date, or today."
    },
    {
     "name": "horizon_days",
     "title": "Horizon (days)",
     "type": "Whole number",
     "description": "Number of days to project."
    }
   ],
   "outputs": [
    {
     "id": "items",
     "label": "Items",
     "description": "Per item: stock, safety stock, demand and receipts, cover, run-out date, below safety stock, shortfall, next receipt, status."
    },
    {
     "id": "projection",
     "label": "Projection",
     "description": "Per item and day: opening stock, demand, receipts, closing stock."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Safety-Stock",
     "name": "Safety Stock and Reorder Point",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Safety-Stock/index.html",
     "note": "Sets the safety stock and reorder levels this block checks against."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Holt-Winters-Forecast",
     "name": "Holt-Winters Forecast",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Holt-Winters-Forecast/index.html",
     "note": "Forecasts the demand to project."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Stock-Run-Out/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Stock-Run-Out",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Stock-Run-Out/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Stock-Run-Out/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Stock-Run-Out/sample-data/movements.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Stock-Run-Out/sample-data/stock.csv"
    ]
   }
  },
  {
   "id": "Analytics/Survival",
   "name": "Survival Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Statistics",
   "description": "Estimates survival (or retention) curves from time-to-event data with censoring, by the Kaplan-Meier or Fleming-Harrington method, overall or per group, with 95% confidence limits and the median time per group.",
   "tags": [
    "survival analysis",
    "kaplan-meier",
    "fleming-harrington",
    "censoring",
    "retention",
    "churn",
    "time to event"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "time",
     "title": "Time",
     "type": "Field",
     "description": "How long each subject was followed: until the event, or until it was last seen without one (a number of days, weeks, months...)."
    },
    {
     "name": "event",
     "title": "Event",
     "type": "Field",
     "description": "Whether the event happened at the end of Time: 1 = event, 0 = censored (still going). 2 = event, 1 = censored, and true / false, are accepted too."
    },
    {
     "name": "strata",
     "title": "Group",
     "type": "Field",
     "description": "Optional: a field whose values each get their own curve (a plan, region or cohort). Leave empty for one curve over all records."
    }
   ],
   "options": [
    {
     "name": "type",
     "title": "Method",
     "type": "Choice",
     "description": "How the curve is estimated. Kaplan-Meier is the standard choice; Fleming-Harrington gives slightly higher values where few subjects remain."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Curve",
     "description": "One record per group and time at which an event or censoring happened, plus time 0: at risk, events, censored, survival and its 95% confidence limits."
    },
    {
     "id": "Summary",
     "label": "Summary",
     "description": "One record per group: records, events, censored, and the median time with its 95% confidence limits (empty when the curve does not fall to 0.5)."
    }
   ],
   "libraries": [],
   "relatedSummary": "No core block estimates survival curves. This block starts from a duration you have already worked out, accepts several event codings and runs on R's survival package, so it can be extended; **Churn Survival Curves** (Python) suits churn from start and end dates.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Churn-Survival",
     "name": "Churn Survival Curves",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Churn-Survival/index.html",
     "note": "Kaplan-Meier retention from start and end dates, with median lifetime, milestone retention and log-rank tests, in Python. Use this block when you have a duration and event flag and want the Fleming-Harrington method or R."
    },
    {
     "relation": "overlaps",
     "kind": "view",
     "id": "survivalcurve",
     "name": "Survival Curve",
     "page": "https://visokio.github.io/omniscope-custom-views/survivalcurve/index.html",
     "note": "Draws Kaplan-Meier curves in a report from start and end dates or a duration, with confidence bands, medians, numbers at risk and a log-rank test. Use this block for the curve as data or for Fleming-Harrington estimates."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Survival/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Survival",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Survival/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Survival/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Survival/sample-data/subscriptions.csv"
    ]
   }
  },
  {
   "id": "Analytics/Text-Word-Frequency",
   "name": "Text Word Frequency",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Text",
   "description": "Word and phrase counts in a text field, overall and per group, with stop words, n-grams and the terms most distinctive of each group.",
   "tags": [
    "text analytics",
    "word frequency",
    "n-grams",
    "word cloud",
    "survey comments",
    "voice of the customer",
    "log-odds",
    "keywords",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "text_field",
     "title": "Text field",
     "type": "Field",
     "description": "The free text to count words in, such as comments."
    },
    {
     "name": "group_fields",
     "title": "Compare groups (optional)",
     "type": "Fields",
     "description": "For example Channel, Product or NPS Category."
    }
   ],
   "options": [
    {
     "name": "max_ngram",
     "title": "Longest phrase (words)",
     "type": "Whole number",
     "description": "1 = single words only."
    },
    {
     "name": "min_word_length",
     "title": "Shortest word (letters)",
     "type": "Whole number"
    },
    {
     "name": "min_documents",
     "title": "Minimum rows using a term",
     "type": "Whole number",
     "description": "Rarer terms are left out."
    },
    {
     "name": "top_n",
     "title": "Top terms per group",
     "type": "Whole number",
     "description": "0 = all."
    },
    {
     "name": "remove_stop_words",
     "title": "Remove English stop words",
     "type": "Yes / no"
    },
    {
     "name": "extra_stop_words",
     "title": "Extra stop words",
     "type": "Text",
     "description": "Comma-separated, for example your brand name."
    },
    {
     "name": "keep_numbers",
     "title": "Keep numbers",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "terms",
     "label": "Terms",
     "description": "Per group and term: occurrences, rows using it, share of rows, rank, share in other groups, distinctiveness z."
    },
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with Word Count, Terms Kept and Top Terms Used."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Text analysis",
     "page": null,
     "note": "Adds word counts, top words, language and readability per record. This block counts words and phrases across records and per group."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Text-Word-Frequency/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Text-Word-Frequency",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Text-Word-Frequency/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Text-Word-Frequency/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Text-Word-Frequency/sample-data/customer-comments.csv"
    ]
   }
  },
  {
   "id": "Analytics/Tiered-Calculation",
   "name": "Tiered Calculation",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Applies tier tables (commission plans, tax bands, volume discounts) marginally or to the whole amount, per plan, with a breakdown by tier.",
   "tags": [
    "tiers",
    "bands",
    "commission",
    "tax bands",
    "volume discount",
    "rebates",
    "tariffs",
    "finance",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "amount_field",
     "title": "Amount field",
     "type": "Field",
     "description": "The amount to apply the tiers to, such as sales."
    },
    {
     "name": "plan_field",
     "title": "Plan field (optional)",
     "type": "Field",
     "description": "Matches the plan key of the tier table."
    },
    {
     "name": "tier_plan_field",
     "title": "Plan key (input 2)",
     "type": "Field",
     "description": "Plan name in the tier table, matched to the plan field of input 1."
    },
    {
     "name": "tier_from_field",
     "title": "From (input 2)",
     "type": "Field",
     "description": "Lower bound of each tier."
    },
    {
     "name": "tier_to_field",
     "title": "To (input 2)",
     "type": "Field",
     "description": "Upper bound of each tier; when empty, the next tier's lower bound."
    },
    {
     "name": "tier_rate_field",
     "title": "Rate (input 2)",
     "type": "Field",
     "description": "Rate for each tier, such as 0.05 for 5%."
    },
    {
     "name": "tier_fixed_field",
     "title": "Fixed amount (input 2)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "mode",
     "title": "Method",
     "type": "Choice",
     "description": "Marginal applies each tier's rate to its own slice, as income tax does; whole applies the reached rate to everything."
    },
    {
     "name": "result_name",
     "title": "Result field name",
     "type": "Text",
     "description": "Name of the new field holding the result."
    },
    {
     "name": "tiers",
     "title": "Tiers (if no input 2)",
     "type": "Text",
     "description": "from-to: rate, for example 0-10000: 0.02, 10000-: 0.05 (add + fixed for a fixed amount)"
    }
   ],
   "outputs": [
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with the result, effective rate and tier reached."
    },
    {
     "id": "breakdown",
     "label": "Breakdown",
     "description": "Per row and tier: band, rate, amount in the tier, result in the tier."
    }
   ],
   "libraries": [],
   "relatedSummary": "Tiered rates are possible with nested IF formulas, but these are hard to maintain when tiers change. This block applies a tier table, marginally or to the whole amount.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Interval-Join",
     "name": "Interval Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Interval-Join/index.html",
     "note": "Looks up the band a value falls in; this block also charges each slice of an amount at its own rate."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Tiered-Calculation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Tiered-Calculation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Tiered-Calculation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Tiered-Calculation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Tiered-Calculation/sample-data/commission-plans.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Tiered-Calculation/sample-data/quarterly-sales.csv"
    ]
   }
  },
  {
   "id": "Analytics/Time-Series-Anomalies",
   "name": "Time Series Anomaly Detection",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Time Series",
   "description": "Flags spikes and dips per series against a robust rolling median and MAD baseline with weekday effects, centred or trailing.",
   "tags": [
    "anomaly detection",
    "outliers",
    "time series",
    "monitoring",
    "robust z-score",
    "MAD",
    "alerts",
    "data quality",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each value."
    },
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "Not needed when counting rows."
    },
    {
     "name": "series_fields",
     "title": "One series per (optional)",
     "type": "Fields",
     "description": "For example Region or Store."
    }
   ],
   "options": [
    {
     "name": "period",
     "title": "Period",
     "type": "Choice"
    },
    {
     "name": "aggregation",
     "title": "Aggregate by",
     "type": "Choice"
    },
    {
     "name": "seasonality",
     "title": "Weekday effect (daily data)",
     "type": "Choice",
     "description": "Allows for regular weekday patterns, so a busy Saturday is not flagged. Automatic tests whether the pattern is there."
    },
    {
     "name": "mode",
     "title": "Baseline",
     "type": "Choice",
     "description": "Centred uses periods on both sides, for reviewing history. Trailing uses only earlier ones, as a live check would."
    },
    {
     "name": "window",
     "title": "Window (periods)",
     "type": "Whole number",
     "description": "For example 28 days."
    },
    {
     "name": "min_history",
     "title": "Minimum periods to score",
     "type": "Whole number"
    },
    {
     "name": "threshold",
     "title": "Threshold (robust z)",
     "type": "Number",
     "description": "3.5 is a common choice; lower flags more."
    },
    {
     "name": "direction",
     "title": "Flag",
     "type": "Choice"
    }
   ],
   "outputs": [
    {
     "id": "series",
     "label": "Series",
     "description": "Every period: value, expected, bounds, weekday effect, robust z, anomaly flag, deviation."
    },
    {
     "id": "anomalies",
     "label": "Anomalies",
     "description": "Spikes and dips ranked by severity."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per series: periods, spikes, dips, latest value and status, largest z."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Anomaly Detection** block scores records from the fields you select; this block is time-aware, allowing for weekday effects, and reports direction and severity.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Anomaly Detection",
     "page": null,
     "note": "Scores records from the selected fields, optionally within groups. This block compares each point with a rolling baseline over time."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Seasonal-Decomposition",
     "name": "Seasonal Decomposition",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Seasonal-Decomposition/index.html",
     "note": "Trend, seasonal and residual parts of each series."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Outlier-Flagger",
     "name": "Outlier Flagger",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Outlier-Flagger/index.html",
     "note": "Outlier bounds per field and group when time order doesn't matter."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "spcchart",
     "name": "SPC Control Chart",
     "page": "https://visokio.github.io/omniscope-custom-views/spcchart/index.html",
     "note": "Flags unusual points in a measure over time with control limits and run rules (XmR), for operational monitoring."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Time-Series-Anomalies/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Time-Series-Anomalies",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Time-Series-Anomalies/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Time-Series-Anomalies/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Time-Series-Anomalies/sample-data/daily-orders.csv"
    ]
   }
  },
  {
   "id": "Analytics/Trade-Area-Summary",
   "name": "Trade Area Summary",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Geo",
   "description": "Counts and sums points within several radii of each site, with exclusive and shared catchment, and the nearest site and distance band per point.",
   "tags": [
    "trade area",
    "catchment",
    "radius",
    "haversine",
    "store network",
    "geo",
    "location analytics",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "site_field",
     "title": "Site name",
     "type": "Field",
     "description": "Names each site, such as a store or branch."
    },
    {
     "name": "site_lat",
     "title": "Latitude",
     "type": "Field",
     "description": "Latitude of each site, in decimal degrees."
    },
    {
     "name": "site_lon",
     "title": "Longitude",
     "type": "Field",
     "description": "Longitude of each site, in decimal degrees."
    },
    {
     "name": "point_lat",
     "title": "Latitude",
     "type": "Field",
     "description": "Latitude of each point in input 2, in decimal degrees."
    },
    {
     "name": "point_lon",
     "title": "Longitude",
     "type": "Field",
     "description": "Longitude of each point in input 2, in decimal degrees."
    },
    {
     "name": "point_value",
     "title": "Value (optional)",
     "type": "Field",
     "description": "For example annual spend or households."
    },
    {
     "name": "point_id",
     "title": "Point ID (optional)",
     "type": "Field",
     "description": "Identifies each point in the per-point output."
    }
   ],
   "options": [
    {
     "name": "radii",
     "title": "Radii",
     "type": "Text",
     "description": "Comma-separated, for example: 1, 3, 5, 10"
    },
    {
     "name": "unit",
     "title": "Unit",
     "type": "Choice",
     "description": "Unit for the radii and distances."
    }
   ],
   "outputs": [
    {
     "id": "sites",
     "label": "Sites",
     "description": "Per site and radius: points and value within, shares, exclusive and shared points, average distance."
    },
    {
     "id": "points",
     "label": "Points",
     "description": "The points with nearest site, distance, distance band and number of sites within the largest radius."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Find-Nearest",
     "name": "Find Nearest",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Find-Nearest/index.html",
     "note": "The k nearest sites for each point, with a distance limit."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Point-To-Point-Distance",
     "name": "Point-to-Point Distance",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-To-Point-Distance/index.html",
     "note": "Distance between a pair of locations on each row."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Trade-Area-Summary/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Trade-Area-Summary",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Trade-Area-Summary/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Trade-Area-Summary/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Trade-Area-Summary/sample-data/branches.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Trade-Area-Summary/sample-data/customers.csv"
    ]
   }
  },
  {
   "id": "Analytics/Validation/Model-Validation",
   "name": "Model Validation",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Validation",
   "description": "Checks a classifier's predictions against the actual values: a confusion matrix, and precision, recall and F1 score for each class with macro, weighted and micro averages.",
   "tags": [
    "model",
    "validation",
    "confusion",
    "matrix",
    "precision",
    "recall",
    "accuracy",
    "fscore",
    "classification"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "ground_truth",
     "title": "Actual values",
     "type": "Field",
     "description": "The field with the true category of each record, for example what was found later."
    },
    {
     "name": "prediction",
     "title": "Predicted values",
     "type": "Field",
     "description": "The field with the category the model predicted."
    }
   ],
   "options": [],
   "outputs": [
    {
     "id": "confusion",
     "label": "Matrix",
     "description": "One record per actual class, one field per predicted class: the number of records in each combination."
    },
    {
     "id": "precision",
     "label": "Scores",
     "description": "Precision, recall and F1 score for each class, then the macro, weighted and micro averages."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Prediction** block gives a confidence per prediction but does not compare its predictions with actual answers; doing that by hand takes a Formula and an Aggregate for accuracy, and more steps per class. This block does it in one step for predictions from any source, with the confusion matrix and per-class scores.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Prediction/SVM",
     "name": "Support Vector Machine",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/SVM/index.html",
     "note": "Predicts categories; validate it on records held back from training."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Prediction/KNN",
     "name": "K-Nearest-Neighbours",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Prediction/KNN/index.html",
     "note": "Predicts categories from the nearest known records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Regression",
     "name": "Regression",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Regression/index.html",
     "note": "Logistic regression, whose predicted classes this block can check."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Forecast-Accuracy",
     "name": "Forecast Accuracy",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Forecast-Accuracy/index.html",
     "note": "For predicted amounts rather than categories: MAE, RMSE, MAPE and bias."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "modelevaluation",
     "name": "Model Evaluation",
     "page": "https://visokio.github.io/omniscope-custom-views/modelevaluation/index.html",
     "note": "For two-class models that give a score: ROC, gains and a confusion matrix at a threshold you move."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Validation/Model-Validation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Validation/Model-Validation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Validation/Model-Validation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Validation/Model-Validation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Validation/Model-Validation/sample-data/ticket-predictions.csv"
    ]
   }
  },
  {
   "id": "Analytics/Variance-Bridge",
   "name": "Variance Bridge",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Finance",
   "description": "Bridges two totals (budget vs actual, year on year) by category as waterfall steps with favourable / adverse labels, per group.",
   "tags": [
    "variance analysis",
    "budget vs actual",
    "waterfall",
    "bridge",
    "management accounts",
    "favourable",
    "adverse",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "category_field",
     "title": "Category field",
     "type": "Field",
     "description": "Each category becomes a step of the bridge, such as a cost line."
    },
    {
     "name": "group_field",
     "title": "One bridge per (optional)",
     "type": "Field"
    },
    {
     "name": "base_field",
     "title": "Base value (for example Budget)",
     "type": "Field",
     "description": "The value the bridge starts from."
    },
    {
     "name": "current_field",
     "title": "Current value (for example Actual)",
     "type": "Field",
     "description": "The value the bridge ends at."
    }
   ],
   "options": [
    {
     "name": "base_label",
     "title": "Base label",
     "type": "Text"
    },
    {
     "name": "current_label",
     "title": "Current label",
     "type": "Text"
    },
    {
     "name": "better_when",
     "title": "Better when",
     "type": "Choice",
     "description": "Sets which variances are favourable: higher for revenue, lower for costs."
    },
    {
     "name": "order",
     "title": "Order steps by",
     "type": "Choice"
    },
    {
     "name": "top_n",
     "title": "Steps before Other",
     "type": "Whole number",
     "description": "0 = all categories."
    }
   ],
   "outputs": [
    {
     "id": "bridge",
     "label": "Bridge",
     "description": "Steps: base total, category variances, current total, with bar start / end, direction and share of variance."
    },
    {
     "id": "categories",
     "label": "Categories",
     "description": "Per category: base, current, variance, variance %, favourable / adverse."
    }
   ],
   "libraries": [],
   "relatedSummary": "A bridge between two totals is possible with aggregates, joins and formulas; this block produces the waterfall steps with favourable and adverse labels in one step.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Price-Volume-Mix",
     "name": "Price-Volume-Mix Bridge",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Price-Volume-Mix/index.html",
     "note": "Splits a revenue change into volume, mix, price, new and lost product effects."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Period-Comparison",
     "name": "Period Comparison",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Period-Comparison/index.html",
     "note": "Month, QTD, YTD and moving annual totals against prior periods."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "variancechart",
     "name": "Variance Chart",
     "page": "https://visokio.github.io/omniscope-custom-views/variancechart/index.html",
     "note": "Shows actual against plan per row or period, with absolute and percentage variance, from the same two measures."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Waterfall",
     "page": null,
     "note": "Draws the bridge steps as a waterfall chart."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Variance-Bridge/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Variance-Bridge",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Variance-Bridge/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Variance-Bridge/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Variance-Bridge/sample-data/cost-centre-budget.csv"
    ]
   }
  },
  {
   "id": "Analytics/Websites/Website-Analysis",
   "name": "Website Analysis",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Website",
   "description": "Follows the links of a website from a start page and lists its pages with their title, heading, text and status, the links on each page, and the images.",
   "tags": [
    "http",
    "website",
    "scraping",
    "crawler",
    "links",
    "broken links",
    "seo",
    "analysis"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "url",
     "title": "Website address",
     "type": "Text",
     "description": "The page to start from, for example https://www.example.com/. https:// is added if you leave it out."
    },
    {
     "name": "max_depth",
     "title": "Link depth",
     "type": "Whole number",
     "description": "How many links to follow away from the start page. 0 reads only the start page."
    },
    {
     "name": "max_pages",
     "title": "Maximum pages",
     "type": "Whole number",
     "description": "The run stops after this many pages, so a large site doesn't run for hours."
    },
    {
     "name": "n_workers",
     "title": "Pages fetched at once",
     "type": "Whole number",
     "description": "How many pages are requested at the same time (1 to 20)."
    },
    {
     "name": "respect_robots",
     "title": "Respect robots.txt",
     "type": "Yes / no",
     "description": "Skip pages that the site's robots.txt asks crawlers not to fetch."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Pages",
     "description": "One record per address visited: url, text, title, h1, depth, status, content_type, final_url and error."
    },
    {
     "id": "Output Data 2",
     "label": "Links",
     "description": "Every link on every page read: parent (the page), url, link_text, and internal (true when it stays on the site)."
    },
    {
     "id": "Output Data 3",
     "label": "Images",
     "description": "Every image on every page read: page, image (its address) and alt (its alternative text)."
    }
   ],
   "libraries": [],
   "relatedSummary": "Omniscope's data sources read data you point them at. This block finds the pages itself, following a site's links from a start page, and returns the pages, links and images as three tables.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "networkanalyser",
     "name": "Network Analyser",
     "page": "https://visokio.github.io/omniscope-custom-views/networkanalyser/index.html",
     "note": "Analyses the Links output (page to page) as a network: PageRank, communities and shortest paths."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Websites/Website-Analysis/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Websites/Website-Analysis",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Websites/Website-Analysis/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Websites/Website-Analysis/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Websites/Website-Analysis/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Analytics/Weighted-Scoring",
   "name": "Weighted Scoring",
   "category": "Analytics",
   "categories": [
    "Analytics"
   ],
   "subcategory": "Decision support",
   "description": "Multi-criteria scorecards: normalise criteria, apply weights, rank alternatives, apply must-pass thresholds and test how sensitive the winner is to the weights.",
   "tags": [
    "weighted scoring",
    "scorecard",
    "multi-criteria",
    "MCDA",
    "supplier selection",
    "tender evaluation",
    "site selection",
    "ranking",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "name_field",
     "title": "Alternative name field",
     "type": "Field",
     "description": "Names each option being scored, such as a supplier."
    },
    {
     "name": "criteria_fields",
     "title": "Criteria fields",
     "type": "Fields",
     "description": "Numeric fields to score on. Not needed when input 2 lists the criteria."
    },
    {
     "name": "lower_better_fields",
     "title": "Lower is better",
     "type": "Fields",
     "description": "Criteria where a lower value is better, such as price or risk."
    },
    {
     "name": "group_fields",
     "title": "Rank within (optional)",
     "type": "Fields",
     "description": "For example a lot or category."
    },
    {
     "name": "crit_name_field",
     "title": "Criterion name",
     "type": "Field",
     "description": "Must match a field of input 1."
    },
    {
     "name": "crit_weight_field",
     "title": "Weight",
     "type": "Field",
     "description": "Weight of each criterion; scaled to sum to 100%."
    },
    {
     "name": "crit_direction_field",
     "title": "Better when (Higher / Lower)",
     "type": "Field",
     "description": "Higher or Lower: which way is better for the criterion."
    },
    {
     "name": "crit_min_field",
     "title": "Minimum (must pass)",
     "type": "Field",
     "description": "Lowest acceptable value; options below it fail."
    },
    {
     "name": "crit_max_field",
     "title": "Maximum (must pass)",
     "type": "Field",
     "description": "Highest acceptable value; options above it fail."
    }
   ],
   "options": [
    {
     "name": "weights",
     "title": "Weights",
     "type": "Text",
     "description": "'Field: weight' pairs, or numbers in criteria order. Empty = equal weights. Weights are scaled to sum to 100%."
    },
    {
     "name": "method",
     "title": "Normalisation",
     "type": "Choice",
     "description": "How each criterion is put on a common 0 to 100 scale before weighting."
    },
    {
     "name": "missing",
     "title": "Missing values",
     "type": "Choice",
     "description": "How an option with no value for a criterion is scored."
    }
   ],
   "outputs": [
    {
     "id": "scores",
     "label": "Scores",
     "description": "Per alternative: 0-100 score and weighted points per criterion, total, thresholds passed, rank, gap to leader."
    },
    {
     "id": "criteria",
     "label": "Criteria",
     "description": "Per criterion: weight, share, direction, thresholds, averages and the weight change that would change the leader."
    },
    {
     "id": "contributions",
     "label": "Contributions",
     "description": "Long form: alternative, criterion, points; for stacked bar charts."
    }
   ],
   "libraries": [],
   "relatedSummary": "The community **Standardise** block is the first step of a scorecard; this block does the whole scorecard: normalise, weight, rank, apply must-pass thresholds and test how sensitive the ranking is to the weights.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Standardisation/Standardise",
     "name": "Standardise",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Standardisation/Standardise/index.html",
     "note": "Min-max rescaling of fields, the first step of a scorecard."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Weighted-Scoring/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Analytics/Weighted-Scoring",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Weighted-Scoring/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Analytics/Weighted-Scoring/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Weighted-Scoring/sample-data/criteria.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Analytics/Weighted-Scoring/sample-data/supplier-bids.csv"
    ]
   }
  },
  {
   "id": "Connectors/AI-Chat-Anthropic-Claude",
   "name": "AI Chat - Claude",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Sends a prompt from each record to an Anthropic Claude model and adds the reply as a Response field. Uses your own Anthropic API key.",
   "tags": [
    "llm",
    "ai",
    "prompt",
    "text generation",
    "classification",
    "anthropic",
    "claude"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Prompt",
     "title": "User prompt field",
     "type": "Field",
     "description": "The field holding each record's prompt: the instruction and the text to work on. Build it with a Formula block if it combines several fields. Numbers and dates are sent as text; records with an empty prompt are not sent."
    },
    {
     "name": "SystemPrompt",
     "title": "System prompt field",
     "type": "Field",
     "description": "Optional. A field holding instructions that set the model's role, rules and answer format, for example \"Reply with one category only: Billing, Delivery or Other\". Usually the same text on every record."
    }
   ],
   "options": [
    {
     "name": "AnthropicApiKey",
     "title": "Anthropic API key",
     "type": "Password",
     "description": "A key from the Claude Console (see https://platform.claude.com/docs/en/get-api-key). Usage is billed to its account."
    },
    {
     "name": "Model",
     "title": "Model",
     "type": "Text",
     "description": "The model ID, as listed at https://platform.claude.com/docs/en/models/overview, for example claude-haiku-5-5 or claude-sonnet-5-5."
    },
    {
     "name": "MaxTokens",
     "title": "Maximum response length (tokens)",
     "type": "Whole number",
     "description": "The most tokens each reply may use; Anthropic requires a limit. A token is roughly three-quarters of a word. Replies that reach the limit are cut short and marked in the Truncated field."
    },
    {
     "name": "Concurrency",
     "title": "Requests at a time",
     "type": "Whole number",
     "description": "How many requests are sent at the same time, 1 to 16. Higher finishes large inputs sooner; lower it if the service reports a rate limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Replies",
     "description": "The input records with two fields added: Response (the model's reply) and Truncated (true if the reply stopped at the maximum response length). Records with an empty prompt have both empty."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **AI Request** block does more (context data, JSON output, diagnostics) but depends on the server's AI settings. This block works where the server has no AI provider or uses another one, flags replies cut short and is a few lines of Python you can change.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "AI Request",
     "page": null,
     "note": "Sends a prompt per record to the AI provider set up on the server, with context fields and structured output. This block calls Claude directly with your own Anthropic key, so nothing needs setting up in the server's AI settings."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/OpenAI-GPT",
     "name": "AI Chat - OpenAI GPT",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/OpenAI-GPT/index.html",
     "note": "Sends the same prompts to OpenAI models, with an OpenAI key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Google-Gemini",
     "name": "AI Chat - Google Gemini",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Google-Gemini/index.html",
     "note": "For Google Gemini, with a Gemini key, which can use a free quota."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-DeepSeek",
     "name": "AI Chat - DeepSeek",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-DeepSeek/index.html",
     "note": "DeepSeek's models, with a DeepSeek key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Local-LLM",
     "name": "AI Chat - Local LLM",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Local-LLM/index.html",
     "note": "For text that must not leave your network: a model on your own server."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Anthropic-Claude/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/AI-Chat-Anthropic-Claude",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Anthropic-Claude/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/AI-Chat-Anthropic-Claude/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Anthropic-Claude/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Anthropic-Claude/sample-data/support-emails.csv"
    ]
   }
  },
  {
   "id": "Connectors/AI-Chat-DeepSeek",
   "name": "AI Chat - DeepSeek",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Sends a prompt from each record to a DeepSeek model and adds the reply as a Response field. Uses your own DeepSeek API key.",
   "tags": [
    "llm",
    "ai",
    "prompt",
    "text generation",
    "classification",
    "deepseek"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Prompt",
     "title": "User prompt field",
     "type": "Field",
     "description": "The field holding each record's prompt: the instruction and the text to work on. Build it with a Formula block if it combines several fields. Numbers and dates are sent as text; records with an empty prompt are not sent."
    },
    {
     "name": "SystemPrompt",
     "title": "System prompt field",
     "type": "Field",
     "description": "Optional. A field holding instructions that set the model's role, rules and answer format, for example \"Reply with one category only: Billing, Delivery or Other\". Usually the same text on every record."
    }
   ],
   "options": [
    {
     "name": "deepseekApiKey",
     "title": "DeepSeek API key",
     "type": "Password",
     "description": "A key from https://platform.deepseek.com/api_keys. Usage is billed to its DeepSeek account."
    },
    {
     "name": "Model",
     "title": "Model",
     "type": "Text",
     "description": "The model ID, as listed at https://api-docs.deepseek.com/quick_start/pricing, for example deepseek-flash or deepseek-v4-pro."
    },
    {
     "name": "Temperature",
     "title": "Temperature",
     "type": "Number",
     "description": "Optional, 0 to 2. Lower values make replies more consistent, higher values more varied. Leave empty to use the model's default. DeepSeek ignores it while the model is thinking, which is on by default."
    },
    {
     "name": "MaxTokens",
     "title": "Maximum response length (tokens)",
     "type": "Whole number",
     "description": "Optional. The most tokens each reply may use; in thinking mode this includes the reasoning. A token is roughly three-quarters of a word. Replies that reach the limit are cut short and marked in the Truncated field. Empty: DeepSeek's default."
    },
    {
     "name": "Concurrency",
     "title": "Requests at a time",
     "type": "Whole number",
     "description": "How many requests are sent at the same time, 1 to 16. Higher finishes large inputs sooner; lower it if the service reports a rate limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Replies",
     "description": "The input records with two fields added: Response (the model's reply) and Truncated (true if the reply stopped at the maximum response length). Records with an empty prompt have both empty."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **AI Request** block does more (context data, JSON output, diagnostics) but uses the providers in the server's AI settings, and DeepSeek is not listed there by name. This block flags replies cut short and is a few lines of Python you can change.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "AI Request",
     "page": null,
     "note": "Sends a prompt per record to the AI provider set up on the server, with context fields and structured output. This block calls DeepSeek directly with your own key, with no server AI settings needed."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/OpenAI-GPT",
     "name": "AI Chat - OpenAI GPT",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/OpenAI-GPT/index.html",
     "note": "Sends the same prompts to OpenAI models, with an OpenAI key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Anthropic-Claude",
     "name": "AI Chat - Claude",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Anthropic-Claude/index.html",
     "note": "Claude models from Anthropic, with an Anthropic key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Google-Gemini",
     "name": "AI Chat - Google Gemini",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Google-Gemini/index.html",
     "note": "For Google Gemini, with a Gemini key, which can use a free quota."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Local-LLM",
     "name": "AI Chat - Local LLM",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Local-LLM/index.html",
     "note": "A model on your own server, through an OpenAI-compatible endpoint, so text stays inside."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-DeepSeek/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/AI-Chat-DeepSeek",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-DeepSeek/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/AI-Chat-DeepSeek/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-DeepSeek/sample-data/expense-claims.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-DeepSeek/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/AI-Chat-Google-Gemini",
   "name": "AI Chat - Google Gemini",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Sends a prompt from each record to a Google Gemini model and adds the reply as a Response field. Uses your own Gemini API key.",
   "tags": [
    "llm",
    "ai",
    "prompt",
    "text generation",
    "classification",
    "google",
    "gemini"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Prompt",
     "title": "User prompt field",
     "type": "Field",
     "description": "The field holding each record's prompt: the instruction and the text to work on. Build it with a Formula block if it combines several fields. Numbers and dates are sent as text; records with an empty prompt are not sent."
    },
    {
     "name": "SystemPrompt",
     "title": "System prompt field",
     "type": "Field",
     "description": "Optional. A field holding instructions that set the model's role, rules and answer format, for example \"Reply with one category only: Billing, Delivery or Other\". Usually the same text on every record."
    }
   ],
   "options": [
    {
     "name": "GEMINI_API_KEY",
     "title": "Gemini API key",
     "type": "Password",
     "description": "A key from Google AI Studio, https://aistudio.google.com/apikey."
    },
    {
     "name": "Model",
     "title": "Model",
     "type": "Text",
     "description": "The model ID, as listed at https://ai.google.dev/gemini-api/docs/models, for example gemini-3.5-flash-lite or gemini-3.8-flash."
    },
    {
     "name": "Temperature",
     "title": "Temperature",
     "type": "Number",
     "description": "Optional, 0 to 2. Lower values make replies more consistent, higher values more varied. Leave empty to use the model's default. Google recommends the default (1.0) for Gemini 3 models."
    },
    {
     "name": "MaxTokens",
     "title": "Maximum response length (tokens)",
     "type": "Whole number",
     "description": "Optional. The most tokens each reply may use; for thinking models this includes the thinking. A token is roughly three-quarters of a word. Replies that reach the limit are cut short and marked in the Truncated field. Empty: the model's default."
    },
    {
     "name": "Concurrency",
     "title": "Requests at a time",
     "type": "Whole number",
     "description": "How many requests are sent at the same time, 1 to 16. Higher finishes large inputs sooner; lower it if the service reports a rate limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Replies",
     "description": "The input records with two fields added: Response (the model's reply) and Truncated (true if the reply stopped at the maximum response length). Records with an empty prompt have both empty."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **AI Request** block does more (context data, JSON output, diagnostics) but depends on the server's AI settings. This block works where the server has no AI provider configured, flags replies cut short and is a few lines of Python you can change.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "AI Request",
     "page": null,
     "note": "Sends a prompt per record to the AI provider set up on the server, with context fields and structured output. This block calls Gemini directly with your own key, which can use the key's free quota."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/OpenAI-GPT",
     "name": "AI Chat - OpenAI GPT",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/OpenAI-GPT/index.html",
     "note": "Sends the same prompts to OpenAI models, with an OpenAI key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Anthropic-Claude",
     "name": "AI Chat - Claude",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Anthropic-Claude/index.html",
     "note": "Claude models from Anthropic, with an Anthropic key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-DeepSeek",
     "name": "AI Chat - DeepSeek",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-DeepSeek/index.html",
     "note": "DeepSeek's models, with a DeepSeek key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Local-LLM",
     "name": "AI Chat - Local LLM",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Local-LLM/index.html",
     "note": "A model on your own server, for text that should stay in the organisation."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Google-Gemini/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/AI-Chat-Google-Gemini",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Google-Gemini/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/AI-Chat-Google-Gemini/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Google-Gemini/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Google-Gemini/sample-data/supplier-descriptions.csv"
    ]
   }
  },
  {
   "id": "Connectors/AI-Chat-Local-LLM",
   "name": "AI Chat - Local LLM",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Sends a prompt from each record to a language model on your own server, through an OpenAI-compatible endpoint, and adds the reply as a Response field.",
   "tags": [
    "llm",
    "ai",
    "prompt",
    "text generation",
    "classification",
    "local",
    "self-hosted",
    "openai-compatible"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Prompt",
     "title": "User prompt field",
     "type": "Field",
     "description": "The field holding each record's prompt: the instruction and the text to work on. Build it with a Formula block if it combines several fields. Numbers and dates are sent as text; records with an empty prompt are not sent."
    },
    {
     "name": "SystemPrompt",
     "title": "System prompt field",
     "type": "Field",
     "description": "Optional. A field holding instructions that set the model's role, rules and answer format, for example \"Reply with one category only: Billing, Delivery or Other\". Usually the same text on every record."
    }
   ],
   "options": [
    {
     "name": "ModelEndpoint",
     "title": "Endpoint",
     "type": "Text",
     "description": "The base address of an OpenAI-compatible API, usually ending in /v1, for example http://localhost:8080/v1 (llama.cpp, llamafile) or http://localhost:11434/v1 (Ollama). Resolved on the Omniscope server."
    },
    {
     "name": "ApiKey",
     "title": "API key",
     "type": "Password",
     "description": "Optional. Only if your server asks for one; most local servers don't."
    },
    {
     "name": "Model",
     "title": "Model",
     "type": "Text",
     "description": "The model name your server expects, for example the name listed by its /v1/models page. Servers that hold one model (llama.cpp, llamafile) accept any name."
    },
    {
     "name": "Temperature",
     "title": "Temperature",
     "type": "Number",
     "description": "Optional, 0 to 2. Lower values make replies more consistent, higher values more varied. Leave empty to use the model's default. Some servers cap it at 1 or ignore it."
    },
    {
     "name": "MaxTokens",
     "title": "Maximum response length (tokens)",
     "type": "Whole number",
     "description": "Optional. The most tokens each reply may use. A token is roughly three-quarters of a word. Replies that reach the limit are cut short and marked in the Truncated field. Empty: the server's default."
    },
    {
     "name": "Concurrency",
     "title": "Requests at a time",
     "type": "Whole number",
     "description": "How many requests are sent at the same time, 1 to 16. Higher finishes large inputs sooner; lower it if the service reports a rate limit. Most local servers answer one request at a time unless set up for more."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Replies",
     "description": "The input records with two fields added: Response (the model's reply) and Truncated (true if the reply stopped at the maximum response length). Records with an empty prompt have both empty."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **AI Request** block does more (context data, JSON output, diagnostics) and can use a local model set up as a custom provider in the server's AI settings. This block needs no AI settings, so each workflow can point at its own model server.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "AI Request",
     "page": null,
     "note": "Sends a prompt per record to the AI provider configured on the server, such as OpenAI or Anthropic. This block sends text only to the OpenAI-compatible endpoint you enter, such as Ollama, LM Studio or vLLM on your own network."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/OpenAI-GPT",
     "name": "AI Chat - OpenAI GPT",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/OpenAI-GPT/index.html",
     "note": "Sends the same prompts to OpenAI models, with an OpenAI key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Anthropic-Claude",
     "name": "AI Chat - Claude",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Anthropic-Claude/index.html",
     "note": "Claude models from Anthropic, with an Anthropic key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Google-Gemini",
     "name": "AI Chat - Google Gemini",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Google-Gemini/index.html",
     "note": "For Google Gemini, with a Gemini key, which can use a free quota."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-DeepSeek",
     "name": "AI Chat - DeepSeek",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-DeepSeek/index.html",
     "note": "DeepSeek's models, with a DeepSeek key."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Local-LLM/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/AI-Chat-Local-LLM",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Local-LLM/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/AI-Chat-Local-LLM/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Local-LLM/sample-data/exit-comments.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/AI-Chat-Local-LLM/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Airtable",
   "name": "Airtable",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Reads every record of an Airtable table, or of one of its views, through the Airtable REST API, one record per row.",
   "tags": [
    "airtable",
    "connector",
    "rest api",
    "spreadsheet database",
    "no-code"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "token",
     "title": "Personal access token",
     "type": "Password",
     "description": "An Airtable personal access token with the data.records:read scope and access to the base."
    },
    {
     "name": "base_id",
     "title": "Base ID",
     "type": "Text",
     "description": "The ID of the base: the part of the table's web address that starts with \"app\", for example appA1b2C3d4E5f6G7."
    },
    {
     "name": "table",
     "title": "Table name or ID",
     "type": "Text",
     "description": "The table's name, exactly as shown in Airtable, or its ID (the part of the address that starts with \"tbl\")."
    },
    {
     "name": "view",
     "title": "View",
     "type": "Text",
     "description": "Optional. A view's name or ID (starts with \"viw\"): only the records in that view are read, in its order."
    },
    {
     "name": "filter_formula",
     "title": "Filter formula",
     "type": "Text",
     "description": "Optional. An Airtable formula; only records for which it is true are read, for example {Status} = 'Open'."
    },
    {
     "name": "page_size",
     "title": "Page size",
     "type": "Whole number",
     "description": "Records per request, from 1 to 100 (Airtable's maximum). All pages are read whatever the size."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Records",
     "description": "One record per Airtable record: the record ID, then the table's fields. Lists and objects as JSON text."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** block (HTTP API location) and the **API** block can call the Airtable API, but Airtable returns at most 100 records a page, so a larger table then needs a loop to follow its offset markers. This block does the paging, rate-limit waits and type clean-up for you, and adds views and filter formulas as options.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/JSON/Normalise",
     "name": "JSON Normalise",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Normalise/index.html",
     "note": "Flattens the lists and objects (attachments, linked records) that arrive as JSON text into records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/JSON/Expand_Fields",
     "name": "JSON Expand fields",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Expand_Fields/index.html",
     "note": "Turns a JSON object field into one field per key."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Airtable/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Airtable",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Airtable/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Airtable/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Airtable/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Azure-Data-Lake-Blob",
   "name": "Azure Data Lake Storage Gen2 Blob",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Azure",
   "description": "Reads one CSV or Parquet file (blob) from a container in an Azure storage account, including Data Lake Storage Gen2, using the account's connection string.",
   "tags": [
    "azure",
    "storage",
    "blob",
    "data lake",
    "adls gen2",
    "parquet",
    "csv",
    "connector"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "connectionString",
     "title": "Connection string",
     "type": "Password",
     "description": "The storage account's connection string: in the Azure portal, Access keys, Connection string under key1."
    },
    {
     "name": "containerName",
     "title": "Container name",
     "type": "Text",
     "description": "The name of the blob container (for Data Lake Storage Gen2, the file system). Case-sensitive."
    },
    {
     "name": "blobPath",
     "title": "Blob path",
     "type": "Text",
     "description": "The folders and file name inside the container, for example sales/2026/march.parquet. Case-sensitive."
    },
    {
     "name": "fileType",
     "title": "File type",
     "type": "Choice",
     "description": "Automatic goes by the file extension (.parquet, .pq, .csv, .tsv, .txt, optionally followed by .gz). CSV files are read with a comma separator (a tab for .tsv) and a header row."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Data",
     "description": "The file's records, with the fields in the file."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Azure Gen2 Storage** source is the first place to look. This block is a short Python script around Azure's own library that also reads gzipped files and converts Parquet types (decimals, lists, structs) that need a field type in Omniscope.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Azure Gen2 Storage",
     "page": null,
     "note": "The core source for a remote file on Azure Gen2 Storage. This block reads one CSV or Parquet blob with the storage account's connection string, which can be a read-only shared access signature."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Azure-Data-Lake-Blob/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Azure-Data-Lake-Blob",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Azure-Data-Lake-Blob/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Azure-Data-Lake-Blob/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Azure-Data-Lake-Blob/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/BigQuery/Google-BigQuery-Custom-SQL",
   "name": "Google BigQuery Custom SQL",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "BigQuery",
   "description": "Runs a SQL query (GoogleSQL) on Google BigQuery and outputs the result, with BigQuery's types mapped to Omniscope field types.",
   "tags": [
    "google",
    "bigquery",
    "sql",
    "query",
    "google cloud",
    "connector"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "ApplicationCredentials",
     "title": "JSON key file",
     "type": "File",
     "description": "Optional. The path, on the Omniscope server, of a service account's JSON key file. Leave empty to use the server's own Google credentials (the GOOGLE_APPLICATION_CREDENTIALS environment variable, or the service account of the machine it runs on)."
    },
    {
     "name": "ProjectId",
     "title": "Project",
     "type": "Text",
     "description": "Optional. The Google Cloud project the query runs in and is billed to. Default: the key file's project, or the server's default project."
    },
    {
     "name": "Query",
     "title": "SQL query",
     "type": "Text",
     "description": "A GoogleSQL query, for example SELECT region, SUM(amount) AS amount FROM `my-project.sales.orders` GROUP BY region. Name tables in full: project.dataset.table."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Results",
     "description": "One record per row the query returns, with the query's columns as fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Database** block can reach BigQuery through a JDBC connection, once Google's BigQuery JDBC driver has been set up on the server. The community BigQuery blocks use Google's Python library and a service account key instead.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Connectors/BigQuery/Google-BigQuery-Import-Table",
     "name": "Google BigQuery Import Table",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Import-Table/index.html",
     "note": "Reads a whole table or view, with tables listed page by page rather than billed as a query. Use Custom SQL to filter, join or aggregate in BigQuery first."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Google-BigQuery-Writer",
     "name": "Google BigQuery Export",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Google-BigQuery-Writer/index.html",
     "note": "Writes records back to a BigQuery table."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Custom-SQL/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/BigQuery/Google-BigQuery-Custom-SQL",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/BigQuery/Google-BigQuery-Custom-SQL/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/BigQuery/Google-BigQuery-Custom-SQL/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/BigQuery/Google-BigQuery-Custom-SQL/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/BigQuery/Google-BigQuery-Import-Table",
   "name": "Google BigQuery Import Table",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "BigQuery",
   "description": "Reads a whole Google BigQuery table or view, page by page, with BigQuery's types mapped to Omniscope field types.",
   "tags": [
    "google",
    "bigquery",
    "import",
    "table",
    "google cloud",
    "connector"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "ApplicationCredentials",
     "title": "JSON key file",
     "type": "File",
     "description": "Optional. The path, on the Omniscope server, of a service account's JSON key file. Leave empty to use the server's own Google credentials (the GOOGLE_APPLICATION_CREDENTIALS environment variable, or the service account of the machine it runs on)."
    },
    {
     "name": "TableId",
     "title": "Table ID",
     "type": "Text",
     "description": "The table's ID: project.dataset.table, for example my-project.sales.orders. A view works too."
    },
    {
     "name": "PageSize",
     "title": "Page size",
     "type": "Whole number",
     "description": "Records per request (default 10,000). Larger pages mean fewer requests and more memory per page. All pages are read whatever the size."
    },
    {
     "name": "MaxRows",
     "title": "Maximum records",
     "type": "Whole number",
     "description": "Optional. Read only the first this many records, for example to try out a large table. Empty or 0 reads them all."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Table",
     "description": "The table's records, with its columns as fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Database** block can reach BigQuery through a JDBC connection, once Google's BigQuery JDBC driver has been set up on the server. The community BigQuery blocks use Google's Python library and a service account key instead; this one reads a whole table or view without running (and paying for) a query.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Connectors/BigQuery/Google-BigQuery-Custom-SQL",
     "name": "Google BigQuery Custom SQL",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Custom-SQL/index.html",
     "note": "Runs a GoogleSQL query and outputs only its result. Use Import Table to bring in a table as it is."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Google-BigQuery-Writer",
     "name": "Google BigQuery Export",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Google-BigQuery-Writer/index.html",
     "note": "Writes records back to a BigQuery table."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Import-Table/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/BigQuery/Google-BigQuery-Import-Table",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/BigQuery/Google-BigQuery-Import-Table/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/BigQuery/Google-BigQuery-Import-Table/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/BigQuery/Google-BigQuery-Import-Table/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Dune",
   "name": "Dune",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Loads the result of a saved Dune query, public or your own: its latest stored result, or a fresh execution. Every page of rows is read, with numbers and dates typed from Dune's column types.",
   "tags": [
    "dune",
    "crypto",
    "analytics",
    "blockchain",
    "sql",
    "ethereum",
    "on-chain"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "duneApiKey",
     "title": "Dune API key",
     "type": "Password",
     "description": "An API key from your Dune account's API settings. The block can read the queries this key's account can see."
    },
    {
     "name": "queryId",
     "title": "Query ID",
     "type": "Text",
     "description": "The number after /queries/ in the query's address (241 for https://dune.com/queries/241/388), or the whole address."
    },
    {
     "name": "mode",
     "title": "Query mode",
     "type": "Choice",
     "description": "Retrieve: read the result Dune last stored for the query (fails if it was never run, has changed since, or the result has expired). Execute: run the query now, wait for it, then read the result."
    },
    {
     "name": "performance",
     "title": "Engine size",
     "type": "Choice",
     "description": "For Execute only: the Dune engine to run on. Medium is Dune's default; Large has more resources for heavy queries and can use more credits."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Execution",
     "description": "One record about the run: Execution ID, Query ID, State, Submitted, Started, Ended, Expires, Rows."
    },
    {
     "id": "Output Data 2",
     "label": "Results",
     "description": "The query's rows, every page, with the columns in Dune's order and typed from Dune's column types."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can read one page of a query's stored result from Dune's API, but cannot start an execution and wait for it, follow the next pages or use Dune's column types (dates and large numbers arrive as text). This block does all three.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Connectors/Flipside",
     "name": "Flipside",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flipside/index.html",
     "note": "Runs your own SQL on Flipside's blockchain tables, but Flipside has wound down its public query tools. Use Dune for saved queries on a service that is still running."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Etherscan",
     "name": "Etherscan",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Etherscan/index.html",
     "note": "Balances and transaction lists for a field of Ethereum addresses."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Dune/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Dune",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Dune/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Dune/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Dune/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Etherscan",
   "name": "Etherscan",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Loads the native-coin balances, or the full transaction lists, of the Ethereum addresses in a field, from the Etherscan API (Ethereum and other chains by chain ID).",
   "tags": [
    "blockchain",
    "ethereum",
    "etherscan",
    "crypto",
    "wallet",
    "balances",
    "transactions"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "address",
     "title": "Address field",
     "type": "Field",
     "description": "The field that holds the addresses (0x followed by 40 hexadecimal characters). Repeated addresses are looked up once."
    }
   ],
   "options": [
    {
     "name": "apikey",
     "title": "API key",
     "type": "Password",
     "description": "Your Etherscan API key, from https://etherscan.io/myapikey. One key works for every chain Etherscan serves; the free plan covers selected chains."
    },
    {
     "name": "method",
     "title": "What to load",
     "type": "Choice",
     "description": "Balances: one record per address. Transactions: one record per transaction of each address."
    },
    {
     "name": "chainid",
     "title": "Chain ID",
     "type": "Text",
     "description": "The chain to query by its ID: 1 for Ethereum (the default), 8453 for Base, 42161 for Arbitrum One, and so on (Etherscan's list of supported chains)."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Results",
     "description": "Balances: Account, Balance, Balance (wei). Transactions: Account, Time, Amount and the transaction fields as Etherscan names them."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** and **Batch API** blocks can call Etherscan, but the answers come back as JSON with every number as text and amounts in wei, at most 10,000 transactions per call, and with no pacing for the free plan's rate limit. This block batches the balance calls, pages through long transaction histories and writes typed records.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Dune",
     "name": "Dune",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Dune/index.html",
     "note": "SQL query results over indexed blockchain data, for questions across many addresses."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "orbitaltransactionflow",
     "name": "Orbital Transaction Flow",
     "page": "https://visokio.github.io/omniscope-custom-views/orbitaltransactionflow/index.html",
     "note": "Replays transactions between senders and receivers over time; the transaction lists of a few addresses fit it, with an address at the centre."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Etherscan/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Etherscan",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Etherscan/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Etherscan/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Etherscan/sample-data/addresses.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Etherscan/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Flightstats/Airlines",
   "name": "FlightStats Airlines",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Flightstats",
   "description": "Loads the list of airlines from the FlightStats (Cirium Flex) API, one record per airline with its code (FlightStats, IATA, ICAO), name, phone number and whether it is active. Needs a FlightStats App ID and App key.",
   "tags": [
    "flightstats",
    "airlines",
    "aviation",
    "travel",
    "reference data",
    "cirium"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [],
   "options": [
    {
     "name": "appID",
     "title": "App ID",
     "type": "Text",
     "description": "The application ID of your FlightStats (Cirium Flex) account."
    },
    {
     "name": "appKey",
     "title": "App key",
     "type": "Password",
     "description": "The application key of your FlightStats (Cirium Flex) account. It is sent with each request and never written to the output."
    },
    {
     "name": "includeInactive",
     "title": "Include inactive airlines",
     "type": "Yes / no",
     "description": "Also load airlines that are no longer active (the API's full list rather than the active list). The active field says which is which."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Airlines",
     "description": "One record per airline, with the fields FlightStats returns (codes, name and the rest; see the README)."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can read a JSON web address, but the FlightStats answer is a list wrapped in an object, credentials go in the address and errors come back in a different shape. This block makes the request, checks the answer and writes one record per airline with the same fields every run.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Airports",
     "name": "FlightStats Airports",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airports/index.html",
     "note": "The airport list from the same API, as a lookup for airport codes."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Flights",
     "name": "FlightStats Flights",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Flights/index.html",
     "note": "Looks up scheduled flights by carrier and flight number."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airlines/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Flightstats/Airlines",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Airlines/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Flightstats/Airlines/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Airlines/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Flightstats/Airports",
   "name": "FlightStats Airports",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Flightstats",
   "description": "Loads the list of airports from the FlightStats (Cirium Flex) API, one record per airport with its codes (FlightStats, IATA, ICAO, FAA), name, city, country, time zone, latitude, longitude and elevation. Needs a FlightStats App ID and App key.",
   "tags": [
    "flightstats",
    "airports",
    "aviation",
    "travel",
    "reference data",
    "cirium"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [],
   "options": [
    {
     "name": "appID",
     "title": "App ID",
     "type": "Text",
     "description": "The application ID of your FlightStats (Cirium Flex) account."
    },
    {
     "name": "appKey",
     "title": "App key",
     "type": "Password",
     "description": "The application key of your FlightStats (Cirium Flex) account. It is sent with each request and never written to the output."
    },
    {
     "name": "includeInactive",
     "title": "Include inactive airports",
     "type": "Yes / no",
     "description": "Also load airports that are no longer active (the API's full list rather than the active list). The active field says which is which."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Airports",
     "description": "One record per airport, with the fields FlightStats returns (codes, name and the rest; see the README)."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can read a JSON web address, but the FlightStats answer is a list wrapped in an object, fields missing from every airport in one answer would be missing from the output, and errors come back in a different shape. This block checks the answer and writes the same 25 fields every run.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Airlines",
     "name": "FlightStats Airlines",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airlines/index.html",
     "note": "The airline list from the same API, to turn carrier codes into names."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Flights",
     "name": "FlightStats Flights",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Flights/index.html",
     "note": "Scheduled flights, whose departure and arrival airports this list can place on a map."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airports/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Flightstats/Airports",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Airports/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Flightstats/Airports/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Airports/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Flightstats/Flights",
   "name": "FlightStats Flights",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Flightstats",
   "description": "Looks up each flight in the input (carrier, flight number and departure date) in the FlightStats (Cirium Flex) schedules and adds its scheduled airports, local departure and arrival times, aircraft type and codeshares. Needs a FlightStats App ID and App key.",
   "tags": [
    "flightstats",
    "flights",
    "schedules",
    "aviation",
    "travel",
    "cirium"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "carrierField",
     "title": "Carrier field",
     "type": "Field",
     "description": "Text field with the airline code: the \"LH\" in LH1234. FlightStats, IATA or ICAO codes are accepted."
    },
    {
     "name": "flightNumberField",
     "title": "Flight number field",
     "type": "Field",
     "description": "Field with the number part of the flight: the \"1234\" in LH1234 (text or a whole number, up to 4 digits)."
    },
    {
     "name": "dateField",
     "title": "Departure date field",
     "type": "Field",
     "description": "Date field with the day the flight departs (the date in UTC is used; any time of day is ignored)."
    }
   ],
   "options": [
    {
     "name": "appID",
     "title": "App ID",
     "type": "Text",
     "description": "The application ID of your FlightStats (Cirium Flex) account."
    },
    {
     "name": "appKey",
     "title": "App key",
     "type": "Password",
     "description": "The application key of your FlightStats (Cirium Flex) account. It is sent with each request and never written to the output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Flights",
     "description": "One record per scheduled leg: the requested carrier, number and date, then the schedule's airports, local times, terminals, aircraft and codeshares."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Batch API** block can call a web address per record, but each FlightStats answer is nested JSON (legs, service classes, codeshares) to be parsed afterwards, and repeated flights are requested again. This block makes one request per distinct flight and day, flattens the answer to one record per leg and keeps the requested flight on each record to join back.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Airports",
     "name": "FlightStats Airports",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airports/index.html",
     "note": "Airport names and positions, to join to this block's airport codes."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Flightstats/Airlines",
     "name": "FlightStats Airlines",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Airlines/index.html",
     "note": "Airline names for the carrier codes."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flightstats/Flights/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Flightstats/Flights",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Flights/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Flightstats/Flights/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Flights/sample-data/flight-bookings.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flightstats/Flights/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Flipside",
   "name": "Flipside",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Runs a SQL query on Flipside's blockchain data tables through the Flipside API and writes every page of the result. Flipside has wound down its public query tools, so the API may no longer accept queries.",
   "tags": [
    "flipside",
    "crypto",
    "analytics",
    "blockchain",
    "sql"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "key",
     "title": "API key",
     "type": "Password",
     "description": "Your Flipside API key, from your Flipside account."
    },
    {
     "name": "query",
     "title": "SQL query",
     "type": "Text",
     "description": "The SQL to run on Flipside's data tables. Results are not cached between runs."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Query results",
     "description": "Every row of the query's result, with the columns the query returns."
    }
   ],
   "libraries": [],
   "relatedSummary": "The Flipside API is a submit, poll and page service that the core **API** block cannot drive, so this block uses Flipside's own Python SDK for all three steps, though Flipside has wound down its public query tools.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Connectors/Dune",
     "name": "Dune",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Dune/index.html",
     "note": "Reads saved Dune queries, public or your own, over indexed blockchain data. The alternative if Flipside no longer accepts queries."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Flipside/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Flipside",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flipside/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Flipside/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Flipside/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/HubSpot",
   "name": "HubSpot",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "CRM",
   "description": "Loads HubSpot companies, contacts, leads, deals, lists, forms, form submissions and associations into records, following every page, or searches contacts and companies.",
   "tags": [
    "hubspot",
    "crm",
    "contacts",
    "companies",
    "deals",
    "marketing",
    "forms"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "key",
     "title": "Private app access token",
     "type": "Password",
     "description": "The access token of a HubSpot private app (Settings > Integrations > Private apps), with read scopes for the objects you load."
    },
    {
     "name": "object",
     "title": "Action",
     "type": "Choice",
     "description": "What to load."
    },
    {
     "name": "params",
     "title": "Search text or form ID",
     "type": "Text",
     "description": "For the search actions, the text to look for (in names, e-mail addresses, domains and other default searchable properties). For Form submissions, the form's ID, as listed by the Forms action."
    },
    {
     "name": "properties",
     "title": "Properties",
     "type": "Text",
     "description": "Optional, comma-separated HubSpot property names to load for Companies, Contacts, Leads, Deals and the searches, such as name, domain, industry, annualrevenue. Empty: HubSpot's default properties (for contacts: e-mail, first and last name, lifecycle stage)."
    },
    {
     "name": "from_object_type",
     "title": "From object",
     "type": "Text",
     "description": "For Associations: the object type to start from, such as contacts, companies or deals."
    },
    {
     "name": "to_object_type",
     "title": "To object",
     "type": "Text",
     "description": "For Associations: the object type to find links to."
    },
    {
     "name": "object_ids",
     "title": "From object IDs",
     "type": "Text",
     "description": "For Associations, optional: comma-separated record IDs of the From object. Empty: every record of the From object."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Records",
     "description": "One record per HubSpot record, list, form, submission or association, depending on the action"
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can make one HubSpot request with a token header, but HubSpot returns 100 records a page, each pointing to the next, and nests every property inside a properties object. This block follows the pages, flattens the properties into fields, turns timestamps into dates and also reads the associations between objects.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/HubSpot/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/HubSpot",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/HubSpot/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/HubSpot/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/HubSpot/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Jira",
   "name": "Jira",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Loads the projects in a Jira Cloud site and the issues of a project or a JQL search, one record per issue with its status, people, dates, labels and components.",
   "tags": [
    "jira",
    "issue",
    "bug",
    "atlassian",
    "project management",
    "jql",
    "tickets"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "jiraInstance",
     "title": "Jira site",
     "type": "Text",
     "description": "Your Jira Cloud site: the part before .atlassian.net (your-company for https://your-company.atlassian.net), or the whole address."
    },
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "The e-mail address you sign in to Atlassian with."
    },
    {
     "name": "apiKey",
     "title": "API token",
     "type": "Password",
     "description": "Create one at https://id.atlassian.com/manage-profile/security/api-tokens. The block can see what this account can see."
    },
    {
     "name": "projectID",
     "title": "Project key",
     "type": "Text",
     "description": "Load the issues of this project, such as SHOP (a project id also works). Leave empty to load projects only, or to use the JQL filter alone."
    },
    {
     "name": "jql",
     "title": "JQL filter",
     "type": "Text",
     "description": "Optional Jira Query Language condition, such as status != Done AND created >= -30d. Combined with the project key when both are set; may end with ORDER BY."
    },
    {
     "name": "extraFields",
     "title": "Extra fields",
     "type": "Text",
     "description": "Optional comma-separated Jira field ids to add as fields, such as customfield_10016 or environment. Values are written as text or numbers; objects by their name, lists joined with commas."
    }
   ],
   "outputs": [
    {
     "id": "Projects",
     "label": "Projects",
     "description": "The projects the account can see: id, key, name, type, category"
    },
    {
     "id": "Issues",
     "label": "Issues",
     "description": "One record per issue of the project or JQL filter; empty unless one of them is set"
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** and **Batch API** blocks can call Jira's REST API, but each page of 100 issues is a separate request whose token comes from the previous answer, and the issue fields arrive as nested JSON. This block follows the pages itself and writes one tidy record per issue.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Jira/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Jira",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Jira/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Jira/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Jira/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/OpenAI-GPT",
   "name": "AI Chat - OpenAI GPT",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Sends a prompt from each record to an OpenAI model and adds the reply as a Response field. Uses your own OpenAI API key.",
   "tags": [
    "llm",
    "ai",
    "prompt",
    "text generation",
    "classification",
    "openai",
    "gpt"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Prompt",
     "title": "User prompt field",
     "type": "Field",
     "description": "The field holding each record's prompt: the instruction and the text to work on. Build it with a Formula block if it combines several fields. Numbers and dates are sent as text; records with an empty prompt are not sent."
    },
    {
     "name": "SystemPrompt",
     "title": "System prompt field",
     "type": "Field",
     "description": "Optional. A field holding instructions that set the model's role, rules and answer format, for example \"Reply with one category only: Billing, Delivery or Other\". Usually the same text on every record."
    }
   ],
   "options": [
    {
     "name": "OpenAiApiKey",
     "title": "OpenAI API key",
     "type": "Password",
     "description": "A secret key from https://platform.openai.com/api-keys. Usage is billed to its OpenAI account."
    },
    {
     "name": "Model",
     "title": "Model",
     "type": "Text",
     "description": "The model ID, as listed at https://developers.openai.com/api/docs/models, for example gpt-6-luna or gpt-6.1-sol. It must support the Chat Completions API."
    },
    {
     "name": "Temperature",
     "title": "Temperature",
     "type": "Number",
     "description": "Optional, 0 to 2. Lower values make replies more consistent, higher values more varied. Leave empty to use the model's default. Current GPT models accept a temperature only when reasoning is switched off, so leave it empty for them."
    },
    {
     "name": "MaxTokens",
     "title": "Maximum response length (tokens)",
     "type": "Whole number",
     "description": "Optional. The most tokens each reply may use, including the model's hidden reasoning, so a low limit can leave the reply empty. A token is roughly three-quarters of a word. Replies that reach the limit are cut short and marked in the Truncated field. Empty: the model's own maximum."
    },
    {
     "name": "Concurrency",
     "title": "Requests at a time",
     "type": "Whole number",
     "description": "How many requests are sent at the same time, 1 to 16. Higher finishes large inputs sooner; lower it if the service reports a rate limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Replies",
     "description": "The input records with two fields added: Response (the model's reply) and Truncated (true if the reply stopped at the maximum response length). Records with an empty prompt have both empty."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **AI Request** block does more (context data, JSON output, diagnostics) but depends on the server's AI settings. This block works where the server has no AI provider or uses another one, flags replies cut short and is a few lines of Python you can change.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "AI Request",
     "page": null,
     "note": "Sends a prompt per record to the AI provider set up on the server, with context fields and structured output. This block calls OpenAI directly with your own key, billed to that key's account, with no server AI settings needed."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Anthropic-Claude",
     "name": "AI Chat - Claude",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Anthropic-Claude/index.html",
     "note": "Claude models from Anthropic, with an Anthropic key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Google-Gemini",
     "name": "AI Chat - Google Gemini",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Google-Gemini/index.html",
     "note": "For Google Gemini, with a Gemini key, which can use a free quota."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-DeepSeek",
     "name": "AI Chat - DeepSeek",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-DeepSeek/index.html",
     "note": "DeepSeek's models, with a DeepSeek key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/AI-Chat-Local-LLM",
     "name": "AI Chat - Local LLM",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/AI-Chat-Local-LLM/index.html",
     "note": "Any OpenAI-compatible model server on your own network, so text stays inside."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/OpenAI-GPT/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/OpenAI-GPT",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/OpenAI-GPT/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/OpenAI-GPT/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/OpenAI-GPT/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/OpenAI-GPT/sample-data/product-reviews.csv"
    ]
   }
  },
  {
   "id": "Connectors/Overpass/Street-Coordinates",
   "name": "Overpass Street Coordinates",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Overpass",
   "description": "For each street name in a field, finds the OpenStreetMap streets with that name through the Overpass API, optionally inside a bounding box, and writes one record per point along each street with its latitude and longitude.",
   "tags": [
    "overpass",
    "street",
    "coordinates",
    "map",
    "openstreetmap",
    "geocoding",
    "latitude",
    "longitude"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "addressField",
     "title": "Street name field",
     "type": "Field",
     "description": "The field that holds the street names, such as High Street. Each distinct name is looked up once; empty values are skipped."
    }
   ],
   "options": [
    {
     "name": "matchMode",
     "title": "Match",
     "type": "Choice",
     "description": "Exact name: the street's name equals the value (OpenStreetMap names are case-sensitive). Name contains: the name contains the value, in any case (High Street also finds High Street North)."
    },
    {
     "name": "bbox",
     "title": "Bounding box",
     "type": "Text",
     "description": "Only find streets inside this box: south, west, north, east in decimal degrees, such as 53.77, -1.58, 53.83, -1.50 for central Leeds. Empty: search the whole world (slow; may time out)."
    },
    {
     "name": "endpoint",
     "title": "Overpass API address",
     "type": "Text",
     "description": "The Overpass server to ask. The default is the main public instance; use your own for heavy use."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Points",
     "description": "One record per point along each matching street: the input value, Street name, Way ID, Point (order along the way), Node ID, Latitude, Longitude."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Geocode** block is the one to place a house or a business. This block returns every point OpenStreetMap stores along each street of that name, for when the street itself matters; it builds the Overpass query for each name in a field and retries when the server is busy.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Geocode",
     "page": null,
     "note": "Gives one point per address rather than the shape of the street."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Overpass/Street-Coordinates/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Overpass/Street-Coordinates",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Overpass/Street-Coordinates/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Overpass/Street-Coordinates/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Overpass/Street-Coordinates/sample-data/deliveries.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Overpass/Street-Coordinates/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Salesforce-REST-OAuth",
   "name": "Salesforce REST OAuth",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "CRM",
   "description": "Loads Salesforce data through OAuth 2.0 and the REST API, with a simple object picker that automatically loads all available fields.",
   "tags": [
    "salesforce",
    "crm",
    "oauth",
    "rest",
    "soql"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "authBaseUrl",
     "title": "Salesforce My Domain URL",
     "type": "Text",
     "description": "HTTPS My Domain URL used for OAuth, such as https://acme.my.salesforce.com. My Domain is recommended for External Client Apps."
    },
    {
     "name": "clientId",
     "title": "Client ID / Consumer Key",
     "type": "Text",
     "description": "Consumer Key from the Salesforce External Client App OAuth settings."
    },
    {
     "name": "clientSecret",
     "title": "Client Secret / Consumer Secret",
     "type": "Password",
     "description": "Consumer Secret from the Salesforce External Client App. Omniscope masks this value."
    },
    {
     "name": "operation",
     "title": "Action",
     "type": "Choice",
     "description": "Load data loads records. The two Advanced actions list the objects available, or describe an object's fields, for finding API names."
    },
    {
     "name": "queryMode",
     "title": "Load mode",
     "type": "Choice",
     "description": "Simple loads every field the Run As user can read. Query Builder takes an object, fields, WHERE and ORDER BY; Custom SOQL takes any query."
    },
    {
     "name": "simpleObject",
     "title": "Salesforce object",
     "type": "Choice",
     "description": "For Simple mode: the object to load, with every field the Run As user can read."
    },
    {
     "name": "includeDeleted",
     "title": "Include deleted/archived records",
     "type": "Choice",
     "description": "Also load deleted (in the Recycle Bin) and archived records, through the REST queryAll resource. Used only by the Load data action."
    },
    {
     "name": "maxRecords",
     "title": "Maximum records",
     "type": "Text",
     "description": "Stop after this many records, across all pages; the final message says when the limit was reached. 0 loads every record."
    },
    {
     "name": "objectName",
     "title": "Object API name (advanced)",
     "type": "Text",
     "description": "Used by Query Builder and Describe fields, such as Account or MyObject__c. Simple mode does not use this option."
    },
    {
     "name": "fields",
     "title": "Fields (advanced)",
     "type": "Text",
     "description": "Comma-separated Salesforce field API names used only by Query Builder. Simple mode discovers all fields automatically."
    },
    {
     "name": "whereClause",
     "title": "WHERE clause (advanced)",
     "type": "Text",
     "description": "Optional Query Builder WHERE expression without the word WHERE, such as CreatedDate = LAST_N_DAYS:30."
    },
    {
     "name": "orderBy",
     "title": "ORDER BY clause (advanced)",
     "type": "Text",
     "description": "Optional Query Builder ORDER BY expression without the words ORDER BY, such as CreatedDate DESC."
    },
    {
     "name": "soql",
     "title": "Custom SOQL (advanced)",
     "type": "Text",
     "description": "The query to run when Load mode is Custom SOQL, such as SELECT Id, Name, Owner.Name FROM Account."
    },
    {
     "name": "apiVersion",
     "title": "REST API version",
     "type": "Text",
     "description": "Use 'auto' to discover the latest REST API version exposed by the org, or specify a version such as 67.0."
    },
    {
     "name": "timeoutSeconds",
     "title": "HTTP timeout (seconds)",
     "type": "Text",
     "description": "Timeout for each OAuth/REST request. Transient 429 and 5xx responses are retried automatically."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Records",
     "description": "Salesforce records (date and date-time fields as dates), or the object list or field descriptions of the schema actions"
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Salesforce** block is the first choice for most imports, and **Salesforce output** writes back. This block adds what the core block leaves out: lookups through relationships in one query, deleted and archived records, a record limit and schema listings.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Salesforce",
     "page": null,
     "note": "Reads a Salesforce table or a custom SOQL query. This block signs in with the OAuth 2.0 client credentials flow, for unattended runs, and can load every readable field of an object without SOQL."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Salesforce-REST-OAuth/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Salesforce-REST-OAuth",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Salesforce-REST-OAuth/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Salesforce-REST-OAuth/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Salesforce-REST-OAuth/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Slack-API-WebClient",
   "name": "Slack API WebClient",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Slack",
   "description": "Calls any Slack Web API method, such as conversations.list or chat.postMessage, and turns the answer into records, following every page.",
   "tags": [
    "slack",
    "api",
    "messaging",
    "channels",
    "users",
    "chat"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "authToken",
     "title": "Slack token",
     "type": "Password",
     "description": "The bot or user token (xoxb-... or xoxp-...) from your Slack app's OAuth & Permissions page. It needs the scopes of the method you call."
    },
    {
     "name": "methodName",
     "title": "API method",
     "type": "Text",
     "description": "The Slack Web API method, such as conversations.list, users.list, conversations.history or chat.postMessage. See https://api.slack.com/methods."
    },
    {
     "name": "methodType",
     "title": "HTTP method",
     "type": "Choice",
     "description": "GET for methods that read (arguments from Query Params); POST for methods that change something, such as chat.postMessage (arguments from JSON Payload)."
    },
    {
     "name": "jsonPayload",
     "title": "JSON Payload (POST)",
     "type": "Text",
     "description": "Arguments for a POST, as a JSON object, for example {\"channel\": \"C0123456789\", \"text\": \"Report is ready\"}."
    },
    {
     "name": "params",
     "title": "Query Params (GET)",
     "type": "Text",
     "description": "Arguments for a GET, as a JSON object, for example {\"types\": \"public_channel,private_channel\", \"limit\": 200}."
    },
    {
     "name": "recordsFrom",
     "title": "Records from",
     "type": "Text",
     "description": "The list in the answer to turn into records, such as channels, members or messages. Leave empty to use the first list in the answer."
    },
    {
     "name": "followPages",
     "title": "Follow pages",
     "type": "Yes / no",
     "description": "Repeat the call with Slack's next cursor until the last page, and combine the records. Untick to read the first page only."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Records",
     "description": "One record per item of the list in the answer (or the answer itself), all pages combined"
    },
    {
     "id": "Output Data 2",
     "label": "Raw response",
     "description": "The JSON text of each page Slack returned, one record per page"
    }
   ],
   "libraries": [],
   "relatedSummary": "For a message per record the community **Slack Bot** block is simpler; this block is the general one, calling any Slack Web API method, reading as well as posting, and turning the answer's list into records.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Slack-Bot",
     "name": "Slack Bot",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Slack-Bot/index.html",
     "note": "Posts one message per record to a channel, without writing API calls."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Slack-API-WebClient/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Slack-API-WebClient",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Slack-API-WebClient/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Slack-API-WebClient/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Slack-API-WebClient/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Slim-CD",
   "name": "Slim CD Transaction",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Loads the transactions of a Slim CD payment gateway account (the SearchTransactions2 report), filtered by date range, customer, amount, card or transaction details, one record per transaction.",
   "tags": [
    "slimcd",
    "slim",
    "payments",
    "gateway",
    "transactions",
    "card payments",
    "reconciliation"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "The Slim CD username."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "The Slim CD password (may be empty for API access credentials)."
    },
    {
     "name": "siteid",
     "title": "Site ID",
     "type": "Text",
     "description": "Your Slim CD Site ID (a number)."
    },
    {
     "name": "clientid",
     "title": "Client ID",
     "type": "Text",
     "description": "Optional: limit the search to one client ID."
    },
    {
     "name": "startdate",
     "title": "Start date",
     "type": "Text",
     "description": "Optional: the first day to search, as Slim CD expects it (YYYY-MM-DD)."
    },
    {
     "name": "enddate",
     "title": "End date",
     "type": "Text",
     "description": "Optional: the last day to search, as Slim CD expects it (YYYY-MM-DD)."
    },
    {
     "name": "treat_all_output_as_text",
     "title": "Treat all output fields as text",
     "type": "Yes / no",
     "description": "Ticked: every field is text, exactly as Slim CD sent it. Unticked: the transaction date is a date, amounts are decimals, IDs whole numbers and Card present true/false."
    },
    {
     "name": "recordcountonly",
     "title": "Record count only",
     "type": "Yes / no",
     "description": "Ticked: one record with the number of transactions the search would return, instead of the transactions."
    },
    {
     "name": "maxrecords",
     "title": "Max records",
     "type": "Text",
     "description": "Optional: the most transactions to return (a whole number). Empty: Slim CD's own limit."
    },
    {
     "name": "reverseorder",
     "title": "Reverse order",
     "type": "Yes / no",
     "description": "Ticked: asks Slim CD for the newest transactions first."
    },
    {
     "name": "firstname",
     "title": "First name",
     "type": "Text",
     "description": "Optional filter: the cardholder's first name."
    },
    {
     "name": "lastname",
     "title": "Last name",
     "type": "Text",
     "description": "Optional filter: the cardholder's last name."
    },
    {
     "name": "city",
     "title": "City",
     "type": "Text",
     "description": "Optional filter."
    },
    {
     "name": "state",
     "title": "State",
     "type": "Text",
     "description": "Optional filter."
    },
    {
     "name": "zip",
     "title": "ZIP code",
     "type": "Text",
     "description": "Optional filter."
    },
    {
     "name": "email",
     "title": "E-mail",
     "type": "Text",
     "description": "Optional filter."
    },
    {
     "name": "amount",
     "title": "Amount",
     "type": "Text",
     "description": "Optional filter: the transaction amount, matched as Slim CD matches it."
    },
    {
     "name": "clienttransref",
     "title": "Client transaction reference",
     "type": "Text",
     "description": "Optional filter: your own reference sent with the transaction (ClientTransRef)."
    },
    {
     "name": "transtype",
     "title": "Transaction type",
     "type": "Text",
     "description": "Optional filter: the transaction type as Slim CD names it (TransType)."
    },
    {
     "name": "cardnumber",
     "title": "Card number",
     "type": "Text",
     "description": "Optional filter: the card number as Slim CD matches it (CardNumber)."
    },
    {
     "name": "cardid",
     "title": "Card ID",
     "type": "Text",
     "description": "Optional filter (CardID)."
    },
    {
     "name": "cardpart",
     "title": "Card part",
     "type": "Text",
     "description": "Optional filter, passed to Slim CD as CardPart."
    },
    {
     "name": "clerkname",
     "title": "Clerk name",
     "type": "Text",
     "description": "Optional filter (ClerkName)."
    },
    {
     "name": "gateid",
     "title": "Gateway ID",
     "type": "Text",
     "description": "Optional filter: one transaction's gateway ID (GateID)."
    }
   ],
   "outputs": [
    {
     "id": "Transactions",
     "label": "Transactions",
     "description": "Slim CD transactions returned by the search, one record each (or one record with the count when Record count only is ticked)."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can call Slim CD's reporting address, but the transactions arrive nested in the reply with every value as text, in a different shape for one transaction or none, and the password sits in the URL. This block sends the search from separate options (the password in a password field), checks Slim CD's response code and writes one typed record per transaction.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Slim-CD/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Slim-CD",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Slim-CD/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Slim-CD/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Slim-CD/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Trello",
   "name": "Trello",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Loads Trello boards, lists, cards, checklists and checklist items, and searches cards, with labels, members and due dates as fields.",
   "tags": [
    "trello",
    "board",
    "card",
    "kanban",
    "tasks",
    "project management"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "apiKey",
     "title": "API Key",
     "type": "Password",
     "description": "The API key of a Trello Power-Up: create one at https://trello.com/power-ups/admin, then open its API key page."
    },
    {
     "name": "apiToken",
     "title": "API Token",
     "type": "Password",
     "description": "A token generated from the API key's page (the Token link). The block sees what this token's account can see."
    },
    {
     "name": "workspaceID",
     "title": "Workspace id (boards)",
     "type": "Text",
     "description": "Load the workspace's boards. The id or short name from the workspace address, https://trello.com/w/your-workspace."
    },
    {
     "name": "boardID",
     "title": "Board id (lists and cards)",
     "type": "Text",
     "description": "Load the board's lists and, unless a list id is set, all its open cards. The id, or the short code from the board address (https://trello.com/b/AbCd1234)."
    },
    {
     "name": "listID",
     "title": "List id (cards of one list)",
     "type": "Text",
     "description": "Load only the open cards of this list into Cards. A list's id is in the board's .json address."
    },
    {
     "name": "cardID",
     "title": "Card id (checklists)",
     "type": "Text",
     "description": "Load the card's checklists, with how many items each has and how many are complete. The id, or the short code from the card address."
    },
    {
     "name": "checklistID",
     "title": "Checklist id (items)",
     "type": "Text",
     "description": "Load the checklist's items with their state (complete or incomplete)."
    },
    {
     "name": "searchString",
     "title": "Search cards",
     "type": "Text",
     "description": "Find cards by text across the boards the token can see (Trello's search syntax, such as label:Blog or due:week). Up to 1,000 cards."
    }
   ],
   "outputs": [
    {
     "id": "Boards",
     "label": "Boards",
     "description": "The workspace's boards (Workspace id)"
    },
    {
     "id": "Lists",
     "label": "Lists",
     "description": "The board's lists (Board id)"
    },
    {
     "id": "Cards",
     "label": "Cards",
     "description": "Cards of the list, or of the whole board, with the list name, labels, members, due dates and checklist counts"
    },
    {
     "id": "Checklists",
     "label": "Checklists",
     "description": "The card's checklists, with item counts (Card id)"
    },
    {
     "id": "Items",
     "label": "Items",
     "description": "The checklist's items and their state (Checklist id)"
    },
    {
     "id": "Search results",
     "label": "Search results",
     "description": "Cards matching the search"
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** block can read one Trello request, but the cards answer gives labels and checklists as nested JSON, and list names need a second request joined on. This block makes those requests, writes labels and members as text and adds each card's list name.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Trello/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Trello",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Trello/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Trello/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Trello/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/Weather/OpenWeatherMap",
   "name": "OpenWeatherMap",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": "Weather",
   "description": "Adds the current weather, or a 5-day forecast in 3-hour steps, from OpenWeatherMap to each location in the input, found by latitude and longitude, city or postcode. Needs a free OpenWeatherMap API key.",
   "tags": [
    "weather",
    "map",
    "longitude",
    "latitude",
    "forecast",
    "openweathermap",
    "temperature",
    "api"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "indexField",
     "title": "Field to identify records",
     "type": "Field",
     "description": "Optional. Copied into each output record as 'index', so the weather can be joined back to the input (a site or customer ID, for example)."
    },
    {
     "name": "latitudeField",
     "title": "Latitude field",
     "type": "Field",
     "description": "Latitude in decimal degrees. Used when finding locations by latitude and longitude."
    },
    {
     "name": "longitudeField",
     "title": "Longitude field",
     "type": "Field",
     "description": "Longitude in decimal degrees. Used when finding locations by latitude and longitude."
    },
    {
     "name": "cityField",
     "title": "City field",
     "type": "Field",
     "description": "City name, such as Leeds. Used when finding locations by city."
    },
    {
     "name": "stateField",
     "title": "State field",
     "type": "Field",
     "description": "Optional. US state code, such as NY; OpenWeatherMap uses states for the United States only."
    },
    {
     "name": "postcodeField",
     "title": "Postcode field",
     "type": "Field",
     "description": "Postcode or zip code. Used when finding locations by postcode."
    },
    {
     "name": "countryField",
     "title": "Country code field (ISO 3166)",
     "type": "Field",
     "description": "Optional. Two-letter ISO 3166 country code, such as GB. Used with city or postcode searches."
    }
   ],
   "options": [
    {
     "name": "appid",
     "title": "API key",
     "type": "Password",
     "description": "Your OpenWeatherMap API key (openweathermap.org, under API keys)."
    },
    {
     "name": "applyRateLimit",
     "title": "Apply free account rate limit",
     "type": "Yes / no",
     "description": "Waits 1.1 seconds between calls so a free account stays under 60 calls a minute. Untick only if your plan allows more."
    },
    {
     "name": "apiChoice",
     "title": "Weather",
     "type": "Choice",
     "description": "Current weather: one record per location. 5-day forecast: 40 records per location, one every 3 hours."
    },
    {
     "name": "searchChoice",
     "title": "Find locations by",
     "type": "Choice",
     "description": "How each record's location is given. Latitude and longitude is the supported way; OpenWeatherMap has deprecated searching by city or postcode (still answered, no longer maintained)."
    },
    {
     "name": "unitsChoice",
     "title": "Units",
     "type": "Choice",
     "description": "Units for temperatures and wind speed: Celsius with metres a second, Fahrenheit with miles an hour, or Kelvin with metres a second."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Weather",
     "description": "Current weather: one record per location found. Forecast: 40 records per location. OpenWeatherMap's fields (temperatures in main.*, wind.*, rain.1h, dt and others; times in UTC), then the location fields from the input and 'index'."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** and **Batch API** blocks can call OpenWeatherMap one location at a time, but the answer is nested JSON whose fields change with the weather. This block asks once per distinct location, keeps to the free plan's call rate and writes a fixed set of fields with times as dates.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Geocode",
     "page": null,
     "note": "Turns addresses into latitude and longitude, the supported way to find locations here."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Weather/OpenWeatherMap/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/Weather/OpenWeatherMap",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Weather/OpenWeatherMap/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/Weather/OpenWeatherMap/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Weather/OpenWeatherMap/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/Weather/OpenWeatherMap/sample-data/sites.csv"
    ]
   }
  },
  {
   "id": "Connectors/XPT-Reader",
   "name": "XPT Reader",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "Reads a SAS transport file (XPORT version 5, .xpt): one dataset as records, plus a list of the datasets in the file and their fields.",
   "tags": [
    "xpt",
    "xport",
    "sas",
    "sas transport",
    "cdisc",
    "clinical trials",
    "file"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "xptFile",
     "title": "XPT file",
     "type": "File",
     "description": "The SAS transport file (.xpt) on the Omniscope server."
    },
    {
     "name": "datasetName",
     "title": "Dataset to read",
     "type": "Text",
     "description": "The dataset's name in the file, such as ADSL (upper or lower case). Empty: the first dataset. The Summary output lists the names."
    },
    {
     "name": "convertDates",
     "title": "Read SAS dates as dates",
     "type": "Yes / no",
     "description": "Ticked: numbers with a SAS date format (DATE9., YYMMDD10. and the like) or datetime format (DATETIME20., E8601DT.) become Date fields. Unticked: they stay as SAS numbers (days or seconds since 1 January 1960)."
    }
   ],
   "outputs": [
    {
     "id": "summary",
     "label": "Summary",
     "description": "One record per dataset in the file: name, label, record count, field count, field names, and when it was created and modified."
    },
    {
     "id": "dataset",
     "label": "Dataset",
     "description": "The records of the chosen dataset (the first one if none is chosen)."
    },
    {
     "id": "variables",
     "label": "Fields",
     "description": "One record per field of every dataset: SAS name, label, type, length, format, and how the block reads it."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** block reads delimited text, spreadsheets, JSON, Parquet and other common formats, but not SAS transport files, which are binary with numbers in IBM mainframe floating point. This block reads them directly, lists every dataset and field with its SAS label and format, and turns SAS date numbers into dates.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/XPT-Reader/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/XPT-Reader",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/XPT-Reader/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/XPT-Reader/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/XPT-Reader/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Connectors/YahooFinance",
   "name": "Yahoo Finance",
   "category": "Connectors",
   "categories": [
    "Connectors"
   ],
   "subcategory": null,
   "description": "For each record, loads the price history of the ticker in a field from Yahoo Finance: open, high, low, close, volume, dividends and splits per day, week, month or shorter interval, between two dates.",
   "tags": [
    "stock",
    "finance",
    "ticker",
    "price",
    "yahoo",
    "shares",
    "market data",
    "time series"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Ticker",
     "title": "Ticker field",
     "type": "Field",
     "description": "The field with the ticker symbols, as Yahoo Finance writes them (VOD.L, BMW.DE, ^FTSE). Each distinct symbol is downloaded once; every record with it gets its prices."
    }
   ],
   "options": [
    {
     "name": "Start",
     "title": "Start date",
     "type": "Text",
     "description": "The first day, written YYYY-MM-DD (included)."
    },
    {
     "name": "End",
     "title": "End date",
     "type": "Text",
     "description": "The day after the last one, written YYYY-MM-DD (not included). Empty: up to today."
    },
    {
     "name": "Interval",
     "title": "Interval",
     "type": "Choice",
     "description": "One record per this period. Intraday intervals (1 hour and shorter) only reach back about 60 days (1 hour: about two years)."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Prices",
     "description": "Each input record repeated for every period of its ticker, with Date (or Datetime for intraday), Open, High, Low, Close, Volume, Dividends, Stock Splits."
    }
   ],
   "libraries": [],
   "relatedSummary": "Yahoo Finance has no official API to call from the core **API** or **Batch API** blocks. This block uses the open-source yfinance library to download each ticker's history and repeats your input records for every period, so a holdings table becomes a price history per holding in one step.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "tradingview",
     "name": "TradingView",
     "page": "https://visokio.github.io/omniscope-custom-views/tradingview/index.html",
     "note": "Embeds TradingView's chart for a symbol in the data; it shows TradingView's prices, not the ones this block loads."
    },
    {
     "relation": "seeAlso",
     "kind": "coreView",
     "id": null,
     "name": "Candlestick",
     "page": null,
     "note": "Draws the open, high, low and close prices per period."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/YahooFinance/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Connectors/YahooFinance",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/YahooFinance/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Connectors/YahooFinance/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/YahooFinance/sample-data/holdings.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Connectors/YahooFinance/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Custom-scripts/ExecuteCommand",
   "name": "Execute Command",
   "category": "Code & AI",
   "categories": [
    "Code & AI"
   ],
   "subcategory": null,
   "description": "Runs a program on the machine that runs Omniscope, once or once per record, passes the input records through and outputs each command's exit code, output and errors.",
   "tags": [
    "command",
    "execute",
    "command line",
    "script",
    "batch",
    "terminal"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "commands_field",
     "title": "Field with commands",
     "type": "Field",
     "description": "A field with one command per record. Records with an empty value are skipped. Only use with data you trust."
    }
   ],
   "options": [
    {
     "name": "command",
     "title": "Command",
     "type": "Text",
     "description": "The program and its arguments, for example python --version. Leave empty to take commands from a field."
    },
    {
     "name": "ignoreErrors",
     "title": "Carry on after a failed command",
     "type": "Yes / no",
     "description": "Untick (the default) to stop with an error at the first command that fails. Tick to run the rest and record the failure in the Commands output."
    },
    {
     "name": "auto_cmd",
     "title": "Run through cmd /c (Windows)",
     "type": "Yes / no",
     "description": "Puts \"cmd /c\" in front of each command, so Windows shell commands such as copy and dir work. The command then runs through the Windows shell."
    },
    {
     "name": "timeout",
     "title": "Time limit (seconds)",
     "type": "Whole number",
     "description": "Stop a command that runs longer than this. 0 for no limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Input Data",
     "description": "The input records, unchanged."
    },
    {
     "id": "Output Data 2",
     "label": "Commands",
     "description": "One record per command run: cmd, code (exit code), stdout, stderr, and error (why it failed, empty when it worked)."
    }
   ],
   "libraries": [],
   "relatedSummary": "Omniscope's Scheduler can run a command as a task action, before or after a project refreshes. This block runs commands inside the workflow instead, where they are needed, once or once per record, and returns each command's exit code and output as data. The core **Custom Python script** and **Custom R script** blocks can also start programs, from code you write; this block needs none.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Custom-scripts/ExecuteCommand/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Custom-scripts/ExecuteCommand",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Custom-scripts/ExecuteCommand/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Custom-scripts/ExecuteCommand/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Custom-scripts/ExecuteCommand/sample-data/nightly-steps.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Custom-scripts/ExecuteCommand/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Databases/MongoDB",
   "name": "MongoDB",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": "Databases",
   "description": "Reads the documents of a MongoDB collection, all of them or those matching a JSON query, one record per document.",
   "tags": [
    "mongodb",
    "database",
    "collection",
    "nosql",
    "documents",
    "connector"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "connection_url",
     "title": "Connection URL",
     "type": "Password",
     "description": "Optional. A MongoDB connection string, for example mongodb+srv://user:password@cluster0.example.net/. When given, the connection and authentication settings below are not used."
    },
    {
     "name": "host",
     "title": "Host",
     "type": "Text",
     "description": "The server's name or IP address, for example db.example.com, or 127.0.0.1 for a server on the same machine."
    },
    {
     "name": "port",
     "title": "Port",
     "type": "Whole number",
     "description": "The port MongoDB listens on; usually 27017."
    },
    {
     "name": "user",
     "title": "User",
     "type": "Text",
     "description": "Optional. The user to sign in as; leave empty for a server without authentication."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "The user's password."
    },
    {
     "name": "auth_source",
     "title": "Authentication database",
     "type": "Text",
     "description": "Optional. The database the user was created in. Empty: admin."
    },
    {
     "name": "database",
     "title": "Database",
     "type": "Text",
     "description": "The database that holds the collection."
    },
    {
     "name": "collection",
     "title": "Collection",
     "type": "Text",
     "description": "The collection to read."
    },
    {
     "name": "full_data",
     "title": "Whole collection",
     "type": "Yes / no",
     "description": "Ticked: every document is read and the custom query is ignored. Unticked: only the documents matching the custom query."
    },
    {
     "name": "query",
     "title": "Custom query",
     "type": "Text",
     "description": "A MongoDB filter in JSON, used when \"Whole collection\" is unticked, for example {\"age\": {\"$gte\": 20}} or {\"address.town\": \"Leeds\"}. Names and text need double quotes."
    },
    {
     "name": "limit",
     "title": "Limit",
     "type": "Whole number",
     "description": "Optional. The most documents to read. Empty or 0: no limit."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Documents",
     "description": "One record per document, one field per top-level key; objects and arrays as JSON text."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Database** block reads SQL databases through JDBC; MongoDB is a document database without SQL tables, so it is not one of them. This block reads a collection with MongoDB's own Python driver and turns each document into a record.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/JSON/Normalise",
     "name": "JSON Normalise",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Normalise/index.html",
     "note": "Flattens the nested objects and arrays that arrive as JSON text into records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/JSON/Expand_Fields",
     "name": "JSON Expand fields",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Expand_Fields/index.html",
     "note": "Turns a nested object field into one field per key."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Databases/MongoDB/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Databases/MongoDB",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Databases/MongoDB/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Databases/MongoDB/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Databases/MongoDB/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Databases/Snowflake-custom-query",
   "name": "Snowflake Custom Query",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": "Databases",
   "description": "Runs a SQL query on Snowflake, signing in with a password or a key pair, and outputs the result.",
   "tags": [
    "snowflake",
    "database",
    "sql",
    "query",
    "key-pair",
    "connector"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "account",
     "title": "Account identifier",
     "type": "Text",
     "description": "The part of your Snowflake address before .snowflakecomputing.com, for example myorg-myaccount (or a locator such as xy12345.eu-west-1)."
    },
    {
     "name": "user",
     "title": "User",
     "type": "Text",
     "description": "The Snowflake user to sign in as."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "The user's password. Leave empty when signing in with a private key file."
    },
    {
     "name": "private_key_file",
     "title": "Private key file",
     "type": "File",
     "description": "For key-pair sign-in: the path, on the Omniscope server, of the user's private key (PEM, PKCS#8). Used instead of the password. Keep it outside any shared folder."
    },
    {
     "name": "private_key_password",
     "title": "Private key password",
     "type": "Password",
     "description": "The passphrase of an encrypted private key. Leave empty for an unencrypted key."
    },
    {
     "name": "warehouse",
     "title": "Warehouse",
     "type": "Text",
     "description": "Optional. The warehouse that runs the query, for example COMPUTE_WH. Empty: the user's default warehouse."
    },
    {
     "name": "database",
     "title": "Database",
     "type": "Text",
     "description": "Optional. The database for table names that don't name one, for example SALES. Empty: the user's default."
    },
    {
     "name": "schema",
     "title": "Schema",
     "type": "Text",
     "description": "Optional. The schema for table names that don't name one, for example PUBLIC."
    },
    {
     "name": "role",
     "title": "Role",
     "type": "Text",
     "description": "Optional. The role to use, for example ANALYST. Empty: the user's default role."
    },
    {
     "name": "custom_sql",
     "title": "SQL query",
     "type": "Text",
     "description": "The query to run, for example SELECT REGION, SUM(AMOUNT) AS AMOUNT FROM SALES.PUBLIC.ORDERS GROUP BY REGION."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Results",
     "description": "One record per row the query returns, with the query's columns as fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Database** block connects to Snowflake (driver bundled), supports key-pair sign-in and reads large results without holding them in memory: use it for most Snowflake work. This block is a short script you can read and change, for example to set session parameters, run a statement first or reshape the result in Python.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Database",
     "page": null,
     "note": "Connects to Snowflake among other databases and is the better choice for large tables. This block runs one query through Snowflake's Python connector, with password or key-pair sign-in, and reads the result into memory."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Databases/Snowflake-custom-query/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Databases/Snowflake-custom-query",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Databases/Snowflake-custom-query/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Databases/Snowflake-custom-query/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Databases/Snowflake-custom-query/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Date-Dimension",
   "name": "Date Dimension",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": "Date",
   "description": "Generates a calendar table: calendar and ISO weeks, fiscal periods for any year start, UK or US public holidays, working-day counts and flags.",
   "tags": [
    "date dimension",
    "calendar table",
    "fiscal year",
    "ISO week",
    "bank holidays",
    "working days",
    "time intelligence",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Or take the range from this field (optional)",
     "type": "Field",
     "description": "With data connected, the calendar covers this field's earliest to latest date."
    }
   ],
   "options": [
    {
     "name": "start_date",
     "title": "Start date",
     "type": "Text",
     "description": "For example 2024-01-01. Optional when a date field is chosen."
    },
    {
     "name": "end_date",
     "title": "End date",
     "type": "Text",
     "description": "For example 2026-12-31."
    },
    {
     "name": "whole_years",
     "title": "Round the range out to whole years",
     "type": "Yes / no",
     "description": "Extends a range taken from a field to 1 January and 31 December."
    },
    {
     "name": "fiscal_year_start",
     "title": "Fiscal year starts in month",
     "type": "Whole number",
     "description": "1 = January, 4 = April."
    },
    {
     "name": "fiscal_label",
     "title": "Fiscal year named by",
     "type": "Choice",
     "description": "How the fiscal year is named in the output."
    },
    {
     "name": "region",
     "title": "Public holidays",
     "type": "Choice",
     "description": "Which public holidays are marked and left out of working days."
    },
    {
     "name": "weekend",
     "title": "Weekend",
     "type": "Choice",
     "description": "Days that are not working days."
    },
    {
     "name": "custom_holidays",
     "title": "Extra holidays",
     "type": "Text",
     "description": "Comma-separated, for example: 2025-12-24=Christmas Eve, 2025-12-31=Office closed"
    },
    {
     "name": "reference_date",
     "title": "Reference date (optional)",
     "type": "Text",
     "description": "A date or TODAY, for the relative fields."
    }
   ],
   "outputs": [
    {
     "id": "calendar",
     "label": "Calendar",
     "description": "One row per day with calendar, ISO week, fiscal, holiday and working-day fields."
    },
    {
     "id": "holidays",
     "label": "Holidays",
     "description": "The holidays in the range: date, name, day name, year."
    }
   ],
   "libraries": [],
   "relatedSummary": "Use **Expand Date Fields** for quick date parts on a fact table, and this block for one reusable calendar to join to many tables.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Expand-Date-Fields",
     "name": "Expand Date Fields",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Expand-Date-Fields/index.html",
     "note": "Adds date parts (year, month, quarter, ISO week, day names) to date fields in your data. This block generates a calendar table with fiscal periods, public holidays and working-day counts."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Value sequence",
     "page": null,
     "note": "Generates a date range as a single field."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Working-Days",
     "name": "Working Days",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Working-Days/index.html",
     "note": "Counts and adds working days between dates in your data, with the same UK and US holidays."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Date-Dimension/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Date-Dimension",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Date-Dimension/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Date-Dimension/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Date-Dimension/sample-data/orders.csv"
    ]
   }
  },
  {
   "id": "Inputs/PDF-Reader",
   "name": "PDF Reader",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": null,
   "description": "Extracts the text of every PDF file in a folder, one record per page, with the file name and page number. Optionally writes a .txt file beside each PDF.",
   "tags": [
    "pdf",
    "text",
    "extract",
    "documents",
    "folder",
    "import"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [],
   "options": [
    {
     "name": "folderPath",
     "title": "Folder path",
     "type": "File",
     "description": "The folder of PDF files to read, on the Omniscope server, or a single PDF file."
    },
    {
     "name": "recursive",
     "title": "Include subfolders",
     "type": "Yes / no",
     "description": "Also read PDF files in subfolders; the File field then holds the path from the chosen folder."
    },
    {
     "name": "writeTxt",
     "title": "Extract to TXT files",
     "type": "Yes / no",
     "description": "Also save each PDF's text as a .txt file with the same name, beside the PDF (an existing .txt of that name is replaced). Needs write access to the folder."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Text",
     "description": "One record per PDF page: File, Page, Pages, Extracted text."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** block reads data files such as CSV, Excel and JSON, not the text inside PDF documents. This block reads the text layer of every PDF in a folder, one record per page, for the core text and formula blocks to pick values out of. Scanned pages that are only images need OCR, which it does not do.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/PDF-Reader/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/PDF-Reader",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/PDF-Reader/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/PDF-Reader/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/PDF-Reader/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Rdata",
   "name": "R Data Reader",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": "R",
   "description": "Reads a table from an R data file (.rds, .rda or .RData): a data frame, matrix or vector saved from R. A second output lists the objects in the file.",
   "tags": [
    "rdata",
    "rda",
    "rds",
    "r",
    "data frame",
    "import"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [],
   "options": [
    {
     "name": "rda_file",
     "title": "R data file",
     "type": "File",
     "description": "The .rds, .rda or .RData file to read, on the Omniscope server."
    },
    {
     "name": "obj",
     "title": "Object to load",
     "type": "Text",
     "description": "For .rda and .RData files: the name of the object to load. Leave empty to load the first data frame or matrix in the file. Not used for .rds files, which hold one object."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Data",
     "description": "The chosen object as records: factors as text, dates as date-times, row names as a \"Row name\" field."
    },
    {
     "id": "Output Data 2",
     "label": "Objects",
     "description": "One record per object in the file: Object, Class, Records, Fields, Readable (as a table) and Loaded."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** block does not read R's own save formats; otherwise you write an R script or export to CSV from R first. This block reads one object from a .rds, .rda or .RData file and lists everything else in it.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Rds-Batch-Append",
     "name": "Rds Batch Append",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Rds-Batch-Append/index.html",
     "note": "Appends many .rds files from a folder into one table."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Rdata/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Rdata",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Rdata/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Rdata/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Rdata/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Rds-Batch-Append",
   "name": "Rds Batch Append",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": "R",
   "description": "Appends many .rds files saved from R into one output: every .rds file in a folder, or the files listed in an input field. Files with different fields can be lined up first.",
   "tags": [
    "rds",
    "batch",
    "append",
    "r",
    "folder",
    "import"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "pathField",
     "title": "Path field",
     "type": "Field",
     "description": "When an input is connected: the field holding the full path of each .rds file. Empty paths are skipped."
    }
   ],
   "options": [
    {
     "name": "folder",
     "title": "Folder",
     "type": "File",
     "description": "The folder to read every .rds file from (name ends in .rds, any case). Ignored when an input is connected."
    },
    {
     "name": "recursive",
     "title": "Recursive",
     "type": "Yes / no",
     "description": "Also read .rds files in subfolders."
    },
    {
     "name": "scanFirst",
     "title": "Scan files first",
     "type": "Yes / no",
     "description": "Read every file once to collect all their fields before appending: the output has every field from every file, filled with empty values where a file lacks one. Takes about twice as long. Untick to take the fields from the first file."
    },
    {
     "name": "actionAdditionalFields",
     "title": "Additional fields",
     "type": "Choice",
     "description": "Without Scan files first: what to do when a file has fields the first file did not. Warning and Silently ignore leave those fields out."
    },
    {
     "name": "actionMissingFields",
     "title": "Missing fields",
     "type": "Choice",
     "description": "Without Scan files first: what to do when a file lacks fields the first file had. Warning and Silently ignore fill them with empty values."
    },
    {
     "name": "actionIncorrectFieldType",
     "title": "Incorrect field type",
     "type": "Choice",
     "description": "What to do when a field's type differs from the schema (text in a number field, say): convert the values, with or without a warning, or stop. Values that can't be converted become empty. Whole numbers where decimals are expected are always converted silently."
    },
    {
     "name": "addSourceField",
     "title": "Add source field",
     "type": "Yes / no",
     "description": "Add a field with the path of the file each record came from."
    },
    {
     "name": "sourceFieldName",
     "title": "Source field name",
     "type": "Text",
     "description": "Name of the source field. Empty: Source."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Appended",
     "description": "The records of every file, one after the other, in file name order (or input order), with the fields lined up."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Append** block stacks tables already in the workflow; getting a folder of .rds files in otherwise means an R script of your own. This block reads every .rds file in a folder (or a list of paths) and appends them, lining up fields that differ between files.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Rdata",
     "name": "R Data Reader",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Rdata/index.html",
     "note": "Reads one .rds, .rda or .RData file, including files holding several objects."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Rds-Batch-Append/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Rds-Batch-Append",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Rds-Batch-Append/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Rds-Batch-Append/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Rds-Batch-Append/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/SFTP-Downloader",
   "name": "SFTP Downloader",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": null,
   "description": "Downloads the files in a folder on an SFTP server to the Omniscope server and lists them, ready for an Append files block to load.",
   "tags": [
    "sftp",
    "ssh",
    "download",
    "files",
    "file transfer"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "username",
     "title": "User name",
     "type": "Text",
     "description": "The account to sign in to the SFTP server with."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "The account's password, or the passphrase of the private key. Can be empty when a key without a passphrase is used."
    },
    {
     "name": "privateKeyFile",
     "title": "Private key file",
     "type": "File",
     "description": "Optional: a private key file on the Omniscope server (OpenSSH format) to sign in with instead of, or as well as, the password."
    },
    {
     "name": "hostname",
     "title": "Host name",
     "type": "Text",
     "description": "The SFTP server, such as sftp.example.com."
    },
    {
     "name": "port",
     "title": "Port",
     "type": "Whole number",
     "description": "Usually 22."
    },
    {
     "name": "hostKey",
     "title": "Host key",
     "type": "Text",
     "description": "The server's key fingerprint, such as SHA256:Kx3c..., or its known_hosts line. The block stops if the server offers a different key. Empty: any key is accepted."
    },
    {
     "name": "folder",
     "title": "Folder on the SFTP server",
     "type": "Text",
     "description": "The folder whose files to download, such as /outgoing/daily. Sub-folders are not entered."
    },
    {
     "name": "extension",
     "title": "Files to download",
     "type": "Text",
     "description": "Extensions or patterns, separated by commas: csv, xlsx or sales_*.csv (not case-sensitive). Empty: every file in the folder."
    },
    {
     "name": "localFolder",
     "title": "Download to folder",
     "type": "Text",
     "description": "A folder on the Omniscope server to save the files in (created if missing; files of the same name are replaced). Empty: the block's own working folder."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Files",
     "description": "One record per downloaded file: File path (on the Omniscope server), File name, Size (bytes) and Modified (on the SFTP server, UTC)."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Remote file via SSH** source is the way to read one file. This block fetches a whole folder, or the files matching a pattern such as sales_*.csv, and checks the server's host key.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Remote file via SSH",
     "page": null,
     "note": "Reads one remote file over SFTP or SCP straight into the workflow. This block downloads every file in a folder, or those matching a pattern, for an Append files block to load."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Append files",
     "page": null,
     "note": "Loads the downloaded files, taking their paths from this block."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/SFTP-Downloader/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/SFTP-Downloader",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/SFTP-Downloader/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/SFTP-Downloader/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/SFTP-Downloader/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Sharepoint-Online",
   "name": "SharePoint Online Downloader",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": null,
   "description": "Downloads a file, or every file in a folder, from a SharePoint Online document library to the Omniscope server, ready for an Append files block to load.",
   "tags": [
    "sharepoint",
    "online",
    "office365",
    "microsoft 365",
    "onedrive",
    "microsoft graph",
    "download"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "tenantId",
     "title": "Tenant ID",
     "type": "Text",
     "description": "Your Microsoft Entra tenant: its ID (a GUID) or a domain such as contoso.onmicrosoft.com."
    },
    {
     "name": "clientId",
     "title": "Client ID",
     "type": "Text",
     "description": "The application (client) ID of the app registered in Microsoft Entra ID."
    },
    {
     "name": "clientSecret",
     "title": "Client secret",
     "type": "Password",
     "description": "A client secret value of that app (Certificates & secrets). Secrets expire; create a new one when sign-in starts failing."
    },
    {
     "name": "fileUrl",
     "title": "File or folder URL",
     "type": "Text",
     "description": "The address of a file or folder in a document library, as shown in the browser, for example https://contoso.sharepoint.com/sites/Finance/Shared Documents/Reports/budget.xlsx. A folder downloads every file in it (not sub-folders)."
    },
    {
     "name": "localFolder",
     "title": "Download to folder",
     "type": "Text",
     "description": "A folder on the Omniscope server to save the files in (created if missing; files of the same name are replaced). Empty: the block's own working folder."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Files",
     "description": "One record per downloaded file: File path (on the Omniscope server), File name, Size (bytes), Modified (in SharePoint, UTC) and SharePoint URL."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** and **Append files** blocks read files the Omniscope server can see, but have no SharePoint sign-in. This block signs in through Microsoft Graph and downloads a file or a whole folder from a document library.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Append files",
     "page": null,
     "note": "Loads the downloaded files, taking their paths from this block."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Sharepoint-Online/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Sharepoint-Online",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Sharepoint-Online/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Sharepoint-Online/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Sharepoint-Online/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Inputs/Web-Form-Session-Login",
   "name": "Web Form Session Login",
   "category": "Inputs",
   "categories": [
    "Inputs"
   ],
   "subcategory": null,
   "description": "Logs in to a website through its HTML login form, as a browser would, and outputs the session cookies and every response header, for later HTTP requests that need the session.",
   "tags": [
    "form",
    "session",
    "login",
    "cookies",
    "http",
    "web scraping",
    "authentication"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "url",
     "title": "Login page URL",
     "type": "Text",
     "description": "The address of the page with the login form, such as https://portal.example.com/login."
    },
    {
     "name": "username",
     "title": "User name",
     "type": "Text",
     "description": "Typed into the form's user name or e-mail field (the block picks the likeliest text field)."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Typed into the form's password field."
    },
    {
     "name": "checkLogin",
     "title": "Stop if the login fails",
     "type": "Yes / no",
     "description": "Ticked: stop with a message if the site answers the login with an error status (400 or above) or still shows a password field afterwards. Unticked: always output the headers and cookies, to see what happened."
    }
   ],
   "outputs": [
    {
     "id": "Headers",
     "label": "Headers",
     "description": "One record per response header, for every request and redirect: stage, hop, request URL and cookies sent, status, redirect location, header name and value."
    },
    {
     "id": "Cookies",
     "label": "Cookies",
     "description": "The session's cookies after the login: name, value, domain, path, Secure, Expires (UTC; empty for session cookies) and other attributes."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **API** and **Batch API** blocks send requests with the headers you give them, including a Cookie header, but cannot fill in a login form: they do not read the page, its hidden anti-forgery field or the cookie it sets. This block does that part and outputs the session cookies for those blocks to send.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Web-Form-Session-Login/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Inputs/Web-Form-Session-Login",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Web-Form-Session-Login/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Inputs/Web-Form-Session-Login/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Inputs/Web-Form-Session-Login/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Outputs/Append-PDF-files",
   "name": "Append PDF files",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "PDF",
   "description": "Combines the PDF files listed in the input records, in record order, into one PDF file, with an optional bookmark for each file.",
   "tags": [
    "pdf",
    "append",
    "combine",
    "merge",
    "bookmarks",
    "report pack"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "pdf_file_names",
     "title": "PDF file names",
     "type": "Field",
     "description": "The field with one PDF file name per record, such as report.pdf, or a full path. The files are combined in record order."
    },
    {
     "name": "title",
     "title": "Bookmark titles",
     "type": "Field",
     "description": "Optional: a field with a bookmark title for each file. Records with an empty title get no bookmark; leave the option empty for no bookmarks."
    }
   ],
   "options": [
    {
     "name": "input_folder",
     "title": "Folder of the PDF files",
     "type": "File",
     "description": "The folder holding the PDF files (choosing a file inside it also works). Empty if the file name field holds full paths."
    },
    {
     "name": "output_folder",
     "title": "Output folder",
     "type": "File",
     "description": "The folder to write the combined PDF to. Empty: the folder of the PDF files."
    },
    {
     "name": "file_name",
     "title": "Combined PDF file name",
     "type": "Text",
     "description": "The name of the combined file, such as board-pack.pdf (.pdf is added if missing). An existing file of that name is replaced."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Combined",
     "description": "The input records with the combined PDF's path (Combined PDF), each file's page count (Pages) and the page it starts on in the combined file (First page)."
    }
   ],
   "libraries": [],
   "relatedSummary": "No core block combines PDF files; this block combines a list of PDF files, such as those from **Report tab to PDF**, into one, with bookmarks.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Report-tab-to-PDF",
     "name": "Report tab to PDF",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-tab-to-PDF/index.html",
     "note": "Prints Report tabs to PDF files and lists their paths, ready to combine here."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Append-PDF-files/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Append-PDF-files",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Append-PDF-files/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Append-PDF-files/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Append-PDF-files/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Append-PDF-files/sample-data/report-sections.csv"
    ]
   }
  },
  {
   "id": "Outputs/GitHub",
   "name": "GitHub",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "GitHub",
   "description": "Reads a CSV or other delimited file from a GitHub repository into records, or writes the input records to a file in a repository as a new commit.",
   "tags": [
    "git",
    "github",
    "repository",
    "read",
    "write",
    "csv",
    "commit",
    "version control"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "githubToken",
     "title": "Access token",
     "type": "Password",
     "description": "A GitHub personal access token. Optional to read a public repository; needed to read a private one (Contents: read) and to write (Contents: read and write)."
    },
    {
     "name": "mode",
     "title": "Mode",
     "type": "Choice",
     "description": "Read a file into records, or write the input records to a file as a new commit."
    },
    {
     "name": "compressed",
     "title": "zlib-compressed file",
     "type": "Yes / no",
     "description": "Tick if the file is (or should be written) compressed with zlib. Leave unticked for an ordinary CSV file."
    },
    {
     "name": "separator",
     "title": "Separator",
     "type": "Text",
     "description": "The character between fields: , (the default), ; or | for example; \\t or tab for a tab."
    },
    {
     "name": "repositoryPath",
     "title": "Repository path",
     "type": "Text",
     "description": "owner/repository, such as octo-org/sales-data; a pasted https://github.com/... address also works."
    },
    {
     "name": "filePath",
     "title": "File path",
     "type": "Text",
     "description": "The file within the repository, such as data/sales.csv. Folders that don't exist are created when writing."
    },
    {
     "name": "branch",
     "title": "Branch",
     "type": "Text",
     "description": "Optional. The branch to read from or commit to, such as main; empty uses the repository's default branch."
    },
    {
     "name": "commitMessage",
     "title": "Commit message",
     "type": "Text",
     "description": "Optional, for writing. Empty gives \"Update <file path> from Omniscope\"."
    }
   ],
   "outputs": [
    {
     "id": "Output Data 2",
     "label": "Data",
     "description": "Read: the file's records. Write: one record describing the commit (branch, commit id and address, records written)"
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File** block can read a public file from GitHub by its raw address, but not a private repository's file, and no core output block writes to a Git repository. This block reads public and private files through GitHub's API and writes its input as a commit on a branch, so each published version is kept in the repository's history.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/GitHub/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/GitHub",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/GitHub/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/GitHub/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/GitHub/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Outputs/Google-BigQuery-Writer",
   "name": "Google BigQuery Export",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "BigQuery",
   "description": "Loads the input records into a Google BigQuery table, appending them or replacing the table's contents; the table is created if it doesn't exist.",
   "tags": [
    "google",
    "bigquery",
    "export",
    "load",
    "google cloud",
    "output"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "ApplicationCredentials",
     "title": "JSON key file",
     "type": "File",
     "description": "Optional. The path, on the Omniscope server, of a service account's JSON key file. Leave empty to use the server's own Google credentials (the GOOGLE_APPLICATION_CREDENTIALS environment variable, or the service account of the machine it runs on)."
    },
    {
     "name": "TableId",
     "title": "Table ID",
     "type": "Text",
     "description": "The table's ID: project.dataset.table, for example my-project.sales.orders. Created if it does not exist; the dataset must exist."
    },
    {
     "name": "AppendData",
     "title": "Append records",
     "type": "Yes / no",
     "description": "Ticked: the records are added to the table, whose columns must match. Unticked: the table's records and columns are replaced on every run."
    }
   ],
   "outputs": [],
   "libraries": [],
   "relatedSummary": "The core **Database output** block can write to BigQuery once Google's BigQuery JDBC driver has been set up on the server. This block uses Google's Python library and a service account key instead, sends the records as one BigQuery load job and creates the table if it does not exist.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/BigQuery/Google-BigQuery-Import-Table",
     "name": "Google BigQuery Import Table",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Import-Table/index.html",
     "note": "Reads a BigQuery table back into a workflow."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/BigQuery/Google-BigQuery-Custom-SQL",
     "name": "Google BigQuery Custom SQL",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/BigQuery/Google-BigQuery-Custom-SQL/index.html",
     "note": "Queries BigQuery with GoogleSQL."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Google-BigQuery-Writer/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Google-BigQuery-Writer",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Google-BigQuery-Writer/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Google-BigQuery-Writer/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Google-BigQuery-Writer/sample-data/store-daily-totals.csv"
    ]
   }
  },
  {
   "id": "Outputs/Multi-Form-Upload",
   "name": "Multipart Form Upload",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "API",
   "description": "Posts files to an HTTP endpoint as multipart/form-data, one request per file, and records each response's status and body.",
   "tags": [
    "upload",
    "http",
    "api",
    "multipart",
    "form-data",
    "post",
    "files"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "file_path_field",
     "title": "File path field",
     "type": "Field",
     "description": "With an input connected: the field holding each record's file path on the server. One request per record."
    }
   ],
   "options": [
    {
     "name": "upload_url",
     "title": "Upload URL",
     "type": "Text",
     "description": "The endpoint to POST each file to, starting with http:// or https://."
    },
    {
     "name": "file_path_column",
     "title": "File to upload (no input)",
     "type": "File",
     "description": "The file to upload when no input is connected. With an input, the File path field is used instead."
    },
    {
     "name": "form_field_name",
     "title": "Form field name",
     "type": "Text",
     "description": "The name of the multipart field that carries the file. Most endpoints expect file."
    },
    {
     "name": "extra_fields",
     "title": "Extra form fields",
     "type": "Text",
     "description": "Optional fields sent with every file, one per line as name=value, for example folder=invoices."
    },
    {
     "name": "headers",
     "title": "Headers",
     "type": "Text",
     "description": "Optional HTTP headers, one per line as Name: value, for example Accept: application/json. Saved as plain text in the project: put a secret in Authorization instead."
    },
    {
     "name": "authorization",
     "title": "Authorization",
     "type": "Password",
     "description": "Optional value of the Authorization header, such as Bearer followed by a token. Kept as a password."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Results",
     "description": "One record per file: the input fields (or File path), HTTP status, Response (the body, truncated) and Error (empty when the upload succeeded)."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **File output** block can write a file to a web address with a PUT request, and the **Batch API** block sends one request per record, but neither builds a multipart/form-data body, which upload forms and most file-upload APIs expect. This block builds that body for each file, with the field name, extra fields and headers the endpoint needs; the core **File attributes** block can list the files to send.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Multi-Form-Upload/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Multi-Form-Upload",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Multi-Form-Upload/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Multi-Form-Upload/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Multi-Form-Upload/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Multi-Form-Upload/sample-data/upload-list.csv"
    ]
   }
  },
  {
   "id": "Outputs/Report-tab-to-PDF",
   "name": "Report tab to PDF",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "PDF",
   "description": "Prints a Report tab to a PDF file for each input record, from the tab's address in a field, saving the files in a folder.",
   "tags": [
    "batch",
    "output",
    "pdf",
    "report",
    "print",
    "tab",
    "convert",
    "export"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "report",
     "title": "Report tab URL field",
     "type": "Field",
     "description": "The field holding each Report tab's address, such as http://127.0.0.1:24679/Folder/Project.iox/r/Report/#Tab (a Project metadata block gives these)."
    },
    {
     "name": "fileName",
     "title": "File name field",
     "type": "Field",
     "description": "Optional. The field holding each PDF's file name, without .pdf. Empty: <Report>-<Tab>-<n>.pdf."
    }
   ],
   "options": [
    {
     "name": "outputFolder",
     "title": "Output folder",
     "type": "File",
     "description": "The folder the PDF files are saved in, on the machine running Omniscope. Created if missing; files with the same name are replaced."
    },
    {
     "name": "pageWidth",
     "title": "Page width (pixels)",
     "type": "Whole number",
     "description": "Optional. Overrides the Report's page size (A4, or the Report's fixed size)."
    },
    {
     "name": "pageHeight",
     "title": "Page height (pixels)",
     "type": "Whole number",
     "description": "Optional. Overrides the Report's page size (A4, or the Report's fixed size)."
    },
    {
     "name": "chromeDelay",
     "title": "Loading delay (seconds)",
     "type": "Whole number",
     "description": "Extra seconds to wait after the Report has finished drawing, before printing. Raise it if charts come out half drawn."
    },
    {
     "name": "auth_username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a Report behind Basic Authentication."
    },
    {
     "name": "auth_password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. Used only to open the page; not written to the output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "PDFs",
     "description": "One record per PDF written: URL, PDF filename, PDF path."
    }
   ],
   "libraries": [],
   "relatedSummary": "A Report can be printed by hand from the browser; this block does it from the workflow, one PDF per tab listed in its input, and the core **Batch output** block can email them.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Outputs/Report-to-PDF-batch-output",
     "name": "Multi-tenant Report to PDF",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-PDF-batch-output/index.html",
     "note": "Prints one tab once per scenario of a multi-tenant configuration. Use this block for a list of different tab addresses."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Outputs/Web-Image-PDF-output",
     "name": "Web Image-PDF output",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Web-Image-PDF-output/index.html",
     "note": "Saves screenshots at a chosen size and can put them in one PDF. This block uses Omniscope's print layout on A4 pages instead."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Append-PDF-files",
     "name": "Append PDF files",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Append-PDF-files/index.html",
     "note": "Combines the PDFs this block writes into one pack, with bookmarks."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Project metadata",
     "page": null,
     "note": "Lists a project's Report tabs with their URLs, for this block's input."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-tab-to-PDF/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Report-tab-to-PDF",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-tab-to-PDF/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Report-tab-to-PDF/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-tab-to-PDF/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-tab-to-PDF/sample-data/report-tabs.csv"
    ]
   }
  },
  {
   "id": "Outputs/Report-to-PDF-batch-output",
   "name": "Multi-tenant Report to PDF",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "PDF",
   "description": "Prints one Report tab once per scenario of a multi-tenant configuration, saving a PDF file per scenario in a folder.",
   "tags": [
    "batch",
    "output",
    "pdf",
    "report",
    "print",
    "tab",
    "multi-tenant",
    "scenario",
    "export"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "scenarioField",
     "title": "Scenario field",
     "type": "Field",
     "description": "The field holding the scenario IDs, one per record. Empty: the field called scenario."
    }
   ],
   "options": [
    {
     "name": "report",
     "title": "Report tab URL",
     "type": "Text",
     "description": "The address of the tab to print, without a scenario, such as http://127.0.0.1:24679/Folder/Project.iox/r/Report/#Tab."
    },
    {
     "name": "outputFolder",
     "title": "Output folder",
     "type": "File",
     "description": "The folder the PDF files are saved in, on the machine running Omniscope. Created if missing; files with the same name are replaced."
    },
    {
     "name": "pageWidth",
     "title": "Page width (pixels)",
     "type": "Whole number",
     "description": "Optional. Overrides the Report's page size (A4, or the Report's fixed size)."
    },
    {
     "name": "pageHeight",
     "title": "Page height (pixels)",
     "type": "Whole number",
     "description": "Optional. Overrides the Report's page size (A4, or the Report's fixed size)."
    },
    {
     "name": "chromeDelay",
     "title": "Loading delay (seconds)",
     "type": "Whole number",
     "description": "Extra seconds to wait after the Report has finished drawing, before printing. Raise it if charts come out half drawn."
    },
    {
     "name": "auth_username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a Report behind Basic Authentication."
    },
    {
     "name": "auth_password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. Used only to open the page; not written to the output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "PDFs",
     "description": "One record per PDF written: Scenario, URL, PDF filename, PDF path."
    }
   ],
   "libraries": [],
   "relatedSummary": "A multi-tenant Report already shows each scenario its own data in the browser; this block prints that view to a PDF for every scenario in the configuration, from one tab address, and the core **Batch output** block can email each PDF to its recipient.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Outputs/Report-tab-to-PDF",
     "name": "Report tab to PDF",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-tab-to-PDF/index.html",
     "note": "Prints any list of Report tabs, one PDF per address. Use this block when one tab is needed once per multi-tenant scenario."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-PDF-batch-output/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Report-to-PDF-batch-output",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PDF-batch-output/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Report-to-PDF-batch-output/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PDF-batch-output/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PDF-batch-output/sample-data/scenarios.csv"
    ]
   }
  },
  {
   "id": "Outputs/Report-to-PowerPoint",
   "name": "Report to PowerPoint",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "PowerPoint",
   "description": "Takes a screenshot of each Report tab in the input and makes a PowerPoint file with one slide per tab.",
   "tags": [
    "screenshot",
    "image",
    "pptx",
    "output",
    "print",
    "powerpoint",
    "export",
    "slide",
    "report"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "url_field",
     "title": "URL field",
     "type": "Field",
     "description": "The field holding the address of each Report tab (or other page) to put on a slide."
    }
   ],
   "options": [
    {
     "name": "resolution",
     "title": "Screenshot size",
     "type": "Choice",
     "description": "The size of each screenshot in pixels: Chrome's page area is set to this size (turned on its side for Portrait)."
    },
    {
     "name": "orientation",
     "title": "Orientation",
     "type": "Choice",
     "description": "Landscape: wider than tall, as listed. Portrait: the same size turned on its side."
    },
    {
     "name": "folder_path",
     "title": "Output folder",
     "type": "File",
     "description": "The folder the PowerPoint file and the PNG screenshots are saved in, on the machine running Omniscope. Created if missing; files with the same names are replaced."
    },
    {
     "name": "sleep_seconds",
     "title": "Page load delay (seconds)",
     "type": "Whole number",
     "description": "Extra seconds to wait after the page has finished loading, before the screenshot. Raise it if charts come out half drawn."
    },
    {
     "name": "tinify",
     "title": "Shrink with Tinify",
     "type": "Yes / no",
     "description": "Shrinks each PNG to the width below with the Tinify web service (api.tinify.com), which needs an API key: https://tinify.com/dashboard/api. The screenshots are uploaded to Tinify, so leave it unticked for confidential Reports."
    },
    {
     "name": "tinify_key",
     "title": "Tinify API key",
     "type": "Password",
     "description": "Needed with Shrink with Tinify."
    },
    {
     "name": "tinify_width",
     "title": "Shrunk width (pixels)",
     "type": "Whole number",
     "description": "Needed with Shrink with Tinify. The height keeps the screenshot's proportions."
    },
    {
     "name": "output_file_name",
     "title": "PowerPoint file name",
     "type": "Text",
     "description": "Optional. Empty: report.pptx. .pptx is added if missing."
    },
    {
     "name": "auth_username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For pages behind Basic Authentication."
    },
    {
     "name": "auth_password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. Used only to open the page; not written to the output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Slides",
     "description": "One record per slide: URL, Screenshot path, Screenshot filename, PowerPoint file."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Web-Image-PDF-output",
     "name": "Web Image-PDF output",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Web-Image-PDF-output/index.html",
     "note": "The same screenshots saved as PNG files, or as one PDF."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Project metadata",
     "page": null,
     "note": "Lists a project's Report tabs with their URLs, for this block's input."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-PowerPoint/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Report-to-PowerPoint",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PowerPoint/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Report-to-PowerPoint/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PowerPoint/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-PowerPoint/sample-data/report-tabs.csv"
    ]
   }
  },
  {
   "id": "Outputs/Report-to-Slack",
   "name": "Send Report to Slack",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "Slack",
   "description": "Takes a screenshot of the Report tab at each input record's URL and posts it to a Slack channel with the record's message and the link; optionally also one PDF of all screenshots.",
   "tags": [
    "screenshot",
    "image",
    "pdf",
    "output",
    "print",
    "slack",
    "report",
    "share",
    "notification"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "url_field",
     "title": "Report URL field",
     "type": "Field",
     "description": "The field holding the address of each Report tab (or other page) to post."
    },
    {
     "name": "message",
     "title": "Message field",
     "type": "Field",
     "description": "The field holding each record's message, posted with the screenshot. Web addresses in it become links (a link ends at the first space)."
    }
   ],
   "options": [
    {
     "name": "auth_username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For pages behind Basic Authentication."
    },
    {
     "name": "auth_password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. Used only to open the page; not written to the output."
    },
    {
     "name": "resolution",
     "title": "Screenshot size",
     "type": "Choice",
     "description": "The size of each screenshot in pixels: Chrome's page area is set to this size (turned on its side for Portrait)."
    },
    {
     "name": "orientation",
     "title": "Orientation",
     "type": "Choice",
     "description": "Landscape: wider than tall, as listed. Portrait: the same size turned on its side."
    },
    {
     "name": "sleep_seconds",
     "title": "Page load delay (seconds)",
     "type": "Whole number",
     "description": "Extra seconds to wait after the page has finished loading, before the screenshot. Raise it if charts come out half drawn."
    },
    {
     "name": "create_pdf",
     "title": "Also post a PDF",
     "type": "Yes / no",
     "description": "After the screenshots, post one PDF with a page per screenshot."
    },
    {
     "name": "compression",
     "title": "Compress the PDF",
     "type": "Yes / no",
     "description": "Compresses the PDF's page descriptions (lossless). The screenshots are stored compressed either way, so the file size hardly changes."
    },
    {
     "name": "tinify",
     "title": "Shrink with Tinify",
     "type": "Yes / no",
     "description": "Shrinks each PNG to the width below with the Tinify web service (api.tinify.com), which needs an API key: https://tinify.com/dashboard/api. The screenshots are uploaded to Tinify, so leave it unticked for confidential Reports."
    },
    {
     "name": "tinify_key",
     "title": "Tinify API key",
     "type": "Password",
     "description": "Needed with Shrink with Tinify."
    },
    {
     "name": "tinify_width",
     "title": "Shrunk width (pixels)",
     "type": "Whole number",
     "description": "Needed with Shrink with Tinify. The height keeps the screenshot's proportions."
    },
    {
     "name": "channelID",
     "title": "Channel ID",
     "type": "Text",
     "description": "In the channel's details (at the bottom of the About tab), such as C0123ABCD. The app must be in the channel."
    },
    {
     "name": "authToken",
     "title": "Bot User OAuth token",
     "type": "Password",
     "description": "From the app's OAuth & Permissions page in Slack; it starts with xoxb-."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Results",
     "description": "One record per input record: Row, Status (posted, skipped or error), SlackFileId."
    },
    {
     "id": "Output Data 2",
     "label": "Errors",
     "description": "One record per error: Row (or PDF) and the error. Empty when everything was posted."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Slack-Bot",
     "name": "Slack Bot",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Slack-Bot/index.html",
     "note": "Posts text messages, optionally with a file, without screenshots."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Web-Image-PDF-output",
     "name": "Web Image-PDF output",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Web-Image-PDF-output/index.html",
     "note": "Saves the screenshots as files instead of posting them."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-Slack/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Report-to-Slack",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-Slack/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Report-to-Slack/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-Slack/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Report-to-Slack/sample-data/report-links.csv"
    ]
   }
  },
  {
   "id": "Outputs/Slack-Bot",
   "name": "Slack Bot",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "Slack",
   "description": "Posts one message to a Slack channel for each input record, as your Slack app's bot, optionally with a file attached.",
   "tags": [
    "slack",
    "bot",
    "message",
    "notification",
    "alert",
    "chat"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "message",
     "title": "Message field",
     "type": "Field",
     "description": "The field holding each record's message. Web addresses become links (a link ends at the first space, so write spaces in addresses as %20)."
    }
   ],
   "options": [
    {
     "name": "authToken",
     "title": "Bot User OAuth token",
     "type": "Password",
     "description": "From the app's OAuth & Permissions page in Slack; it starts with xoxb-."
    },
    {
     "name": "channelID",
     "title": "Channel ID",
     "type": "Text",
     "description": "In the channel's details (at the bottom of the About tab), such as C0123ABCD. The app must be in the channel."
    },
    {
     "name": "attachmentPath",
     "title": "File to attach",
     "type": "File",
     "description": "Optional. A file on the machine running Omniscope, attached to every message (needs the files:write scope)."
    },
    {
     "name": "attachmentName",
     "title": "Attachment title",
     "type": "Text",
     "description": "The title Slack shows for the attached file."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Messages",
     "description": "One record per input record: Row, Message (as sent, in Slack's format), AttachmentPath."
    },
    {
     "id": "Output Data 2",
     "label": "Sent",
     "description": "One record per input record: Row, Action (postMessage, upload_with_comment or skip), Channel, Timestamp (Slack's message ts), File ID."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Report-to-Slack",
     "name": "Send Report to Slack",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-Slack/index.html",
     "note": "Posts a screenshot of a Report tab with each message."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Slack-API-WebClient",
     "name": "Slack API WebClient",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Slack-API-WebClient/index.html",
     "note": "Calls any Slack Web API method, to read channels and members or post in other ways."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Telegram",
     "name": "Send to Telegram",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Telegram/index.html",
     "note": "Sends records as a message to a Telegram chat instead."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Slack-Bot/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Slack-Bot",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Slack-Bot/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Slack-Bot/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Slack-Bot/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Slack-Bot/sample-data/stock-alerts.csv"
    ]
   }
  },
  {
   "id": "Outputs/Telegram",
   "name": "Send to Telegram",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "Messenger",
   "description": "Sends the input records as a message to a Telegram chat, group or channel through your bot, as text, a table or JSON.",
   "tags": [
    "telegram",
    "messenger",
    "message",
    "notification",
    "alert",
    "bot",
    "chat"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "bot_token",
     "title": "Bot token",
     "type": "Password",
     "description": "The token @BotFather gave when the bot was created, such as 7000000001:AA..."
    },
    {
     "name": "chat_id",
     "title": "Chat ID",
     "type": "Text",
     "description": "Where to send: a number (a group's starts with -), or @channelname for a public channel. The bot must be in the group or an administrator of the channel."
    },
    {
     "name": "text_format",
     "title": "Format",
     "type": "Choice",
     "description": "How the records are written in the message."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Sent",
     "description": "One record per message sent: Part, Parts, Records (in that message), Characters, Message ID, Chat."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Slack-Bot",
     "name": "Slack Bot",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Slack-Bot/index.html",
     "note": "Posts records as messages to a Slack channel instead."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Email output",
     "page": null,
     "note": "Sends the data by email, with an optional inline table."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Telegram/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Telegram",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Telegram/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Telegram/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Telegram/sample-data/low-stock.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Telegram/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Outputs/Web-Image-PDF-output",
   "name": "Web Image-PDF output",
   "category": "Outputs",
   "categories": [
    "Outputs"
   ],
   "subcategory": "PDF",
   "description": "Saves a screenshot (PNG) of the web page at each input record's URL, at a chosen size, and optionally one PDF with a page per screenshot.",
   "tags": [
    "screenshot",
    "image",
    "pdf",
    "output",
    "print",
    "png",
    "web page",
    "report"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "url_field",
     "title": "URL field",
     "type": "Field",
     "description": "The field holding the address of each page to capture, such as a Report tab URL."
    }
   ],
   "options": [
    {
     "name": "resolution",
     "title": "Screenshot size",
     "type": "Choice",
     "description": "The size of each screenshot in pixels: Chrome's page area is set to this size (turned on its side for Portrait)."
    },
    {
     "name": "orientation",
     "title": "Orientation",
     "type": "Choice",
     "description": "Landscape: wider than tall, as listed. Portrait: the same size turned on its side."
    },
    {
     "name": "folder_path",
     "title": "Output folder",
     "type": "File",
     "description": "The folder the PNG files and the PDF are saved in, on the machine running Omniscope. Created if missing."
    },
    {
     "name": "sleep_seconds",
     "title": "Page load delay (seconds)",
     "type": "Whole number",
     "description": "Extra seconds to wait after the page has finished loading, before the screenshot. Raise it if charts come out half drawn."
    },
    {
     "name": "tinify",
     "title": "Shrink with Tinify",
     "type": "Yes / no",
     "description": "Shrinks each PNG to the width below with the Tinify web service (api.tinify.com), which needs an API key: https://tinify.com/dashboard/api"
    },
    {
     "name": "tinify_key",
     "title": "Tinify API key",
     "type": "Password",
     "description": "Needed with Shrink with Tinify."
    },
    {
     "name": "tinify_width",
     "title": "Shrunk width (pixels)",
     "type": "Whole number",
     "description": "Needed with Shrink with Tinify. The height keeps the screenshot's proportions."
    },
    {
     "name": "create_pdf",
     "title": "Create PDF",
     "type": "Yes / no",
     "description": "Also make one PDF with a page per screenshot, in input order."
    },
    {
     "name": "pdf_file_name",
     "title": "PDF file name",
     "type": "Text",
     "description": "Optional. Empty: screenshots.pdf. .pdf is added if missing."
    },
    {
     "name": "compression",
     "title": "Compress the PDF",
     "type": "Yes / no",
     "description": "Compresses the PDF's page descriptions (lossless). The screenshots are stored compressed either way, so the file size hardly changes."
    },
    {
     "name": "auth_username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For pages behind Basic Authentication."
    },
    {
     "name": "auth_password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. Used only to open the page; not written to the output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Screenshots",
     "description": "One record per screenshot: URL, Screenshot path, Screenshot filename, PDF (its path, or empty)."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block captures what any web page the server can reach shows on screen, Omniscope Reports included, as PNG images at a size you set, with an optional PDF; the core **Batch output** block can attach the images to emails.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Outputs/Report-tab-to-PDF",
     "name": "Report tab to PDF",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-tab-to-PDF/index.html",
     "note": "Prints Report tabs in Omniscope's print layout, one A4 PDF per tab. Use this block for PNG images at a set size, or for any web page."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Report-to-PowerPoint",
     "name": "Report to PowerPoint",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-PowerPoint/index.html",
     "note": "Puts the same kind of screenshots into a PowerPoint deck, one slide per tab."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Outputs/Report-to-Slack",
     "name": "Send Report to Slack",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Report-to-Slack/index.html",
     "note": "Posts screenshots of Report tabs to a Slack channel with a message."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Outputs/Web-Image-PDF-output/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Outputs/Web-Image-PDF-output",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Web-Image-PDF-output/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Outputs/Web-Image-PDF-output/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Web-Image-PDF-output/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Outputs/Web-Image-PDF-output/sample-data/web-pages.csv"
    ]
   }
  },
  {
   "id": "Preparation/Add-row-ID-field",
   "name": "Add row ID field",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Adds a field numbering the records 1, 2, 3 and so on, in the order they arrive, working through the data in chunks so large inputs never have to fit in memory.",
   "tags": [
    "row",
    "id",
    "sequence",
    "record number",
    "index",
    "key",
    "streaming"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "fieldName",
     "title": "ID field name",
     "type": "Text",
     "description": "Name of the new field. Left empty, it is Row ID. An existing field of this name is replaced."
    },
    {
     "name": "startAt",
     "title": "Start at",
     "type": "Whole number",
     "description": "The number given to the first record."
    },
    {
     "name": "putFirst",
     "title": "Put the ID field first",
     "type": "Yes / no",
     "description": "Make the ID the first field. Unticked, it is added as the last field."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Numbered",
     "description": "The input records with the ID field added."
    }
   ],
   "libraries": [],
   "relatedSummary": "Prefer the core **Add row ID** block in new workflows, or a `CURRENTROW()` formula in a **Field organiser** when numbering is one of several field changes. This block has two small extras (the field placed first, an existing ID replaced in place).",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Add row ID",
     "page": null,
     "note": "Does the same job without Python and can restart the numbering within partitions. Prefer it in new workflows; this block streams in chunks and stays for projects that already use it."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Add-row-ID-field/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Add-row-ID-field",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Add-row-ID-field/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Add-row-ID-field/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Add-row-ID-field/sample-data/feedback-cards.csv"
    ]
   }
  },
  {
   "id": "Preparation/Age-And-Tenure",
   "name": "Age and Tenure",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Exact age or service in years, months and days, decimal years, bands, next anniversary and upcoming milestones, with 29 February handled.",
   "tags": [
    "age",
    "tenure",
    "length of service",
    "anniversary",
    "date difference",
    "HR",
    "age bands",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Start date (birth, joining)",
     "type": "Field"
    },
    {
     "name": "end_field",
     "title": "End date (optional)",
     "type": "Field",
     "description": "For example a leaving date."
    }
   ],
   "options": [
    {
     "name": "as_of",
     "title": "As of date",
     "type": "Text",
     "description": "Default: today."
    },
    {
     "name": "bands",
     "title": "Band boundaries (years)",
     "type": "Text"
    },
    {
     "name": "milestones",
     "title": "Milestones (years)",
     "type": "Text"
    },
    {
     "name": "milestone_window_days",
     "title": "Milestone window (days)",
     "type": "Whole number"
    },
    {
     "name": "prefix",
     "title": "Prefix for new fields (optional)",
     "type": "Text",
     "description": "For example 'Age ' or 'Service '."
    }
   ],
   "outputs": [
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with years, months, days, decimal years, band, next anniversary, days to it and upcoming milestone."
    },
    {
     "id": "bands",
     "label": "Bands",
     "description": "Rows and share per band."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Age-And-Tenure/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Age-And-Tenure",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Age-And-Tenure/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Age-And-Tenure/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Age-And-Tenure/sample-data/employees.csv"
    ]
   }
  },
  {
   "id": "Preparation/Anonymise",
   "name": "Anonymise",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Replaces names, emails and other text with generated code words, and shifts numbers and dates by random noise, to make a safer copy of a dataset for demos and testing.",
   "tags": [
    "anonymise",
    "pseudonymise",
    "privacy",
    "masking",
    "test data",
    "noise",
    "personal data"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "selectedFields",
     "title": "Fields to anonymise",
     "type": "Fields",
     "description": "Fields to replace or add noise to. Leave empty to anonymise every field."
    }
   ],
   "options": [
    {
     "name": "epsilon_numeric",
     "title": "Number privacy budget (epsilon)",
     "type": "Number",
     "description": "Lower means more noise on number fields. The typical change is sensitivity ÷ budget."
    },
    {
     "name": "sensitivity_numeric",
     "title": "Number sensitivity",
     "type": "Number",
     "description": "In the fields' own units: how much one record's value may matter. Higher means more noise."
    },
    {
     "name": "epsilon_date",
     "title": "Date privacy budget (epsilon)",
     "type": "Number",
     "description": "Lower means more noise on date fields. The typical shift is sensitivity ÷ budget, in seconds."
    },
    {
     "name": "sensitivity_date",
     "title": "Date sensitivity (seconds)",
     "type": "Number",
     "description": "In seconds (86,400 is one day). Higher means larger date shifts."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Anonymised",
     "description": "The input with the chosen text fields replaced by code words and noise added to the chosen number and date fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Scramble data** block scrambles text in chosen fields; this block keeps more of the data's shape for demos and testing. It is pseudonymisation, so treat the output as still sensitive; the core **Random Sample** block pairs well with it.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Scramble data",
     "page": null,
     "note": "Scrambles text in chosen fields before sharing. This block also adds random noise to numbers and dates, and gives each distinct text value the same code word everywhere, so counts and joins still work."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Anonymise/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Anonymise",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Anonymise/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Anonymise/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Anonymise/sample-data/customers.csv"
    ]
   }
  },
  {
   "id": "Preparation/Business-Hours-SLA",
   "name": "Business Hours and SLA",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Elapsed business hours between timestamps within opening hours, excluding weekends and holidays, with SLA due times and met / breached status.",
   "tags": [
    "business hours",
    "SLA",
    "service desk",
    "working hours",
    "response time",
    "holidays",
    "tickets",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Opened (start) field",
     "type": "Field",
     "description": "When each item was opened."
    },
    {
     "name": "end_field",
     "title": "Resolved (end) field",
     "type": "Field",
     "description": "Empty = still open."
    },
    {
     "name": "priority_field",
     "title": "Priority field (optional)",
     "type": "Field",
     "description": "Priority of each item, for the SLA hours by priority."
    },
    {
     "name": "sla_field",
     "title": "SLA hours field (optional)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "sla_by_priority",
     "title": "SLA hours by priority",
     "type": "Text",
     "description": "For example: P1=4, P2=8, P3=24, P4=40"
    },
    {
     "name": "sla_hours",
     "title": "SLA hours for everything else",
     "type": "Number",
     "description": "0 = none."
    },
    {
     "name": "as_of",
     "title": "As of (for open items)",
     "type": "Text",
     "description": "Default: the latest time in the data."
    },
    {
     "name": "open_time",
     "title": "Opening time",
     "type": "Text",
     "description": "Start of the working day, as HH:MM."
    },
    {
     "name": "close_time",
     "title": "Closing time",
     "type": "Text",
     "description": "End of the working day, as HH:MM."
    },
    {
     "name": "weekend",
     "title": "Weekend",
     "type": "Choice"
    },
    {
     "name": "region",
     "title": "Public holidays",
     "type": "Choice",
     "description": "Public holidays that do not count as working time."
    },
    {
     "name": "custom_holidays",
     "title": "Extra closure dates",
     "type": "Text",
     "description": "Comma-separated, for example: 2025-12-24, 2025-12-31"
    }
   ],
   "outputs": [
    {
     "id": "items",
     "label": "Items",
     "description": "The input with business and calendar hours, open flag, SLA hours, SLA due, SLA status and hours to spare."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per priority and overall: items, closed, open, met, breached, met rate, median hours."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block measures elapsed time within opening hours across weekends, holidays and part days, for SLAs set in business hours, where formulas count only whole working days.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Working-Days",
     "name": "Working Days",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Working-Days/index.html",
     "note": "Counts whole working days and adds them to dates for due dates, with the same holidays. Use this block when the SLA is in hours within opening hours."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Business-Hours-SLA/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Business-Hours-SLA",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Business-Hours-SLA/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Business-Hours-SLA/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Business-Hours-SLA/sample-data/tickets.csv"
    ]
   }
  },
  {
   "id": "Preparation/Canonical-Schema-Mapper",
   "name": "Canonical Schema Mapper",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Maps the many names a field goes by (aliases) to one canonical name, combines fields that hold the same thing, and converts each to a set type with an optional default, following rules kept as data (typed into the block or in a CSV file), working through the data in chunks so large inputs never have to fit in memory.",
   "tags": [
    "field",
    "streaming",
    "rename",
    "name",
    "schema",
    "schema drift",
    "alias",
    "mapping",
    "coalesce",
    "type conversion"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "rulesText",
     "title": "Schema rules",
     "type": "Text",
     "description": "Rules written in the block as CSV, one rule per line or rules separated by semicolons: canonical, aliases, type, default, format (heading row optional). Takes precedence over the file."
    },
    {
     "name": "rules",
     "title": "Schema rules file",
     "type": "File",
     "description": "A CSV file of rules (columns canonical, aliases and, optionally, type, default and format). Used when Schema rules is empty."
    },
    {
     "name": "passthrough_unmapped",
     "title": "Keep unmapped fields",
     "type": "Yes / no",
     "description": "Pass input fields that match no rule through unchanged. Untick to output the canonical fields only."
    },
    {
     "name": "prefer_non_empty_strings",
     "title": "Treat blank text as missing",
     "type": "Yes / no",
     "description": "When combining aliases, skip text that is empty or only spaces and take the next alias's value; blank text also gets the default."
    },
    {
     "name": "auto_normalise_aliases",
     "title": "Match names loosely",
     "type": "Yes / no",
     "description": "Ignore case, and treat spaces, hyphens and underscores alike when matching field names to aliases (Order Date, order_date and ORDER-DATE match). Untick to match names exactly."
    },
    {
     "name": "add_missing_canonical",
     "title": "Output every canonical field",
     "type": "Yes / no",
     "description": "Output all canonical fields, in the rules' order, even when no input field matches one (it is filled with its default, or left empty). Unmapped fields follow. Unticked, only matched canonical fields are output, in the input's order."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Canonical",
     "description": "The input with its fields renamed, combined and converted as the rules say."
    }
   ],
   "libraries": [],
   "relatedSummary": "For fixed renames and types, use the core **Define schema** block. This block is the data-driven route, so one list can drive the same mapping in several workflows and be changed in one place; it streams.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Define schema",
     "page": null,
     "note": "Maps fields and their aliases to set names and types, configured once in the block with no Python. Use this block when the alias rules are kept as data (a CSV file or rules text) shared between projects, or you need defaults and yes/no fields."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Smart-Schema-Normaliser",
     "name": "Smart Schema Normaliser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Schema-Normaliser/index.html",
     "note": "Tidies names and guesses types with no rules at all."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Field-Renamer",
     "name": "Field Renamer",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Field-Renamer/index.html",
     "note": "One-to-one renames from a rules table on another input, without types."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Streaming-Field-Renamer",
     "name": "Streaming Field Renamer",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Streaming-Field-Renamer/index.html",
     "note": "Renames from rules typed in the block or kept in a CSV file, streaming inputs of any size."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Canonical-Schema-Mapper/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Canonical-Schema-Mapper",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Canonical-Schema-Mapper/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Canonical-Schema-Mapper/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Canonical-Schema-Mapper/sample-data/orders-north.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Canonical-Schema-Mapper/sample-data/orders-south.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Canonical-Schema-Mapper/sample-data/schema-rules.csv"
    ]
   }
  },
  {
   "id": "Preparation/Conditional Execution",
   "name": "Conditional Execution",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Workflow",
   "description": "When its yes/no option (usually linked to a project parameter) is on, starts a separate run of the listed blocks of a project; when it is off, does nothing.",
   "tags": [
    "condition",
    "conditional",
    "execution",
    "orchestration",
    "parameter",
    "trigger",
    "rest api"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "Iox_File_Url",
     "title": "Project address",
     "type": "Text",
     "description": "The address of the project whose blocks to run, ending in .iox. On the same server, the internal address such as http://127.0.0.1:24679/Folder/Project.iox/ avoids the public address and its certificate. A report or workflow address of the project is cut back to the project."
    },
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a server that asks for sign-in with a list of users: sent with basic authentication. Leave empty when no sign-in is needed."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. The password for the username."
    },
    {
     "name": "execute",
     "title": "Run the blocks",
     "type": "Yes / no",
     "description": "On: start the blocks. Off: do nothing. Link it to a yes/no project parameter to switch it from outside the workflow."
    },
    {
     "name": "Block_Names",
     "title": "Blocks to run",
     "type": "Text",
     "description": "Names of the blocks to start, separated by commas. Blocks they depend on run too when needed."
    },
    {
     "name": "Refresh_From_Source",
     "title": "Refresh from source",
     "type": "Yes / no",
     "description": "Reload the data from the sources, rather than reuse data the blocks already hold."
    }
   ],
   "outputs": [],
   "libraries": [],
   "relatedSummary": "The core **If-Then-Split**, **Output Router** and **Validate data** blocks decide from the data inside one run: use them whenever the decision depends on data in the workflow. This block decides from a switch, usually linked to a yes/no project parameter set from outside, and starts the listed blocks as a separate run, here or in another project.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "If-Then-Split",
     "page": null,
     "note": "Routes rows from a formula on the data, inside one run."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Output Router",
     "page": null,
     "note": "Sends each row to one output from a formula result, within the run."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Set Project Parameters",
     "name": "Set Project Parameters",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Set%20Project%20Parameters/index.html",
     "note": "Sets the yes/no parameter that switches this block, from within a workflow."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "switchparam",
     "name": "Switch Param",
     "page": "https://visokio.github.io/omniscope-custom-views/switchparam/index.html",
     "note": "Radio buttons in a report that set a parameter, such as the one this block's switch is linked to."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "workflowexecution",
     "name": "Workflow Execution",
     "page": "https://visokio.github.io/omniscope-custom-views/workflowexecution/index.html",
     "note": "A button in a report that starts chosen blocks directly, with no parameter in between."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Conditional%20Execution/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Conditional%20Execution",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Conditional%20Execution/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Conditional%20Execution/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Conditional%20Execution/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Conditional%20Execution/sample-data/stock-levels.csv"
    ]
   }
  },
  {
   "id": "Preparation/Data-Cleansing",
   "name": "Data Cleansing",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Cleansing",
   "description": "Removes empty rows and fields, fixes missing values, strips unwanted characters and changes case in one step, with a report of what changed.",
   "tags": [
    "cleansing",
    "clean",
    "trim",
    "whitespace",
    "nulls",
    "case",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Fields to cleanse",
     "type": "Fields",
     "description": "Leave empty to cleanse every field."
    },
    {
     "name": "case_fields",
     "title": "Change case only in",
     "type": "Fields",
     "description": "Leave empty to change case in every cleansed text field."
    }
   ],
   "options": [
    {
     "name": "remove_null_rows",
     "title": "Remove empty rows",
     "type": "Yes / no",
     "description": "Drop rows where every field is empty or a placeholder."
    },
    {
     "name": "remove_null_columns",
     "title": "Remove empty fields",
     "type": "Yes / no",
     "description": "Drop cleansed fields that have no values at all."
    },
    {
     "name": "null_tokens",
     "title": "Placeholders to treat as missing",
     "type": "Text",
     "description": "Comma-separated values that mean 'no value', such as NULL, N/A, #N/A, -. Matched ignoring case and surrounding spaces."
    },
    {
     "name": "replace_null_text",
     "title": "Replace missing text with blank",
     "type": "Yes / no",
     "description": "Text fields: missing values become empty text. Inside Omniscope empty text and missing look the same; this matters when exporting to systems that tell them apart."
    },
    {
     "name": "replace_null_numbers",
     "title": "Replace missing numbers with 0",
     "type": "Yes / no",
     "description": "Number fields: missing values become 0."
    },
    {
     "name": "trim",
     "title": "Leading and trailing whitespace",
     "type": "Yes / no",
     "description": "Removes spaces at the start and end of text."
    },
    {
     "name": "collapse_whitespace",
     "title": "Tabs, line breaks and repeated spaces",
     "type": "Yes / no",
     "description": "Replaced by a single space."
    },
    {
     "name": "remove_all_whitespace",
     "title": "All whitespace",
     "type": "Yes / no"
    },
    {
     "name": "remove_letters",
     "title": "Letters",
     "type": "Yes / no"
    },
    {
     "name": "remove_numbers",
     "title": "Digits",
     "type": "Yes / no"
    },
    {
     "name": "remove_punctuation",
     "title": "Punctuation",
     "type": "Yes / no"
    },
    {
     "name": "remove_invisible",
     "title": "Invisible and non-printable characters",
     "type": "Yes / no",
     "description": "Zero-width spaces, control characters and byte-order marks are removed; non-breaking spaces become normal spaces."
    },
    {
     "name": "modify_case",
     "title": "Change case",
     "type": "Choice",
     "description": "Changes the case of text, in every cleansed text field or only those chosen below."
    },
    {
     "name": "convert_numbers",
     "title": "Convert number text to numbers",
     "type": "Yes / no",
     "description": "After cleansing, a text field whose values are all numbers (currency symbols and thousands separators allowed) becomes a number field. Values with leading zeros keep the field as text."
    }
   ],
   "outputs": [
    {
     "id": "cleansed",
     "label": "Cleansed",
     "description": "The input after cleansing."
    },
    {
     "id": "report",
     "label": "Report",
     "description": "One row per cleansed field: values changed, placeholders set to missing, missing values filled, and whether the field was removed or converted."
    }
   ],
   "libraries": [],
   "relatedSummary": "Each step is possible with core blocks such as **Normalise case**; this block adds placeholders, invisible characters, missing-value fixes and an audit of what changed.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Smart-Schema-Normaliser",
     "name": "Smart Schema Normaliser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Schema-Normaliser/index.html",
     "note": "Normalises field names, merges duplicate fields and infers types. This block cleans values instead (blanks, characters, case, missing values)."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "Finds missing values, duplicate rows and outliers, and outputs annotated data."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Delete empty data",
     "page": null,
     "note": "Removes empty rows and fields."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Search / replace",
     "page": null,
     "note": "Replaces text, including with regular expressions. This block combines these steps in one checklist."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Cleansing/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Data-Cleansing",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Cleansing/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Data-Cleansing/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Cleansing/sample-data/customer-contacts.csv"
    ]
   }
  },
  {
   "id": "Preparation/Data-Compare",
   "name": "Data Compare",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Audit",
   "description": "Compares two versions of a dataset by key: added, removed and changed records, field-level differences with tolerances, and schema differences.",
   "tags": [
    "compare",
    "diff",
    "reconcile",
    "migration testing",
    "change detection",
    "audit",
    "snapshot",
    "data quality",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "key_fields",
     "title": "Key fields",
     "type": "Fields",
     "description": "Identify a record in both inputs. Leave empty to match whole rows."
    },
    {
     "name": "compare_fields",
     "title": "Fields to compare (optional)",
     "type": "Fields",
     "description": "Default: all fields in both inputs except keys and ignored fields."
    },
    {
     "name": "ignore_fields",
     "title": "Fields to ignore",
     "type": "Fields",
     "description": "For example load timestamps."
    }
   ],
   "options": [
    {
     "name": "tolerance",
     "title": "Numeric tolerance",
     "type": "Number",
     "description": "Differences up to this amount are not changes."
    },
    {
     "name": "relative_tolerance",
     "title": "Relative tolerance",
     "type": "Number",
     "description": "For example 0.001 = 0.1% of the old value."
    },
    {
     "name": "ignore_case",
     "title": "Ignore case",
     "type": "Yes / no",
     "description": "Text that differs only in upper and lower case is not a change."
    },
    {
     "name": "trim",
     "title": "Trim spaces",
     "type": "Yes / no",
     "description": "Spaces at the start or end of text are ignored."
    },
    {
     "name": "blank_is_null",
     "title": "Blank text equals null",
     "type": "Yes / no",
     "description": "Empty text and a missing value count as the same."
    },
    {
     "name": "include_unchanged",
     "title": "List unchanged records",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "records",
     "label": "Records",
     "description": "One row per record: key, status (Added, Removed, Changed, Unchanged, Duplicate key), number and names of changed fields, row numbers."
    },
    {
     "id": "differences",
     "label": "Differences",
     "description": "One row per changed value: key, field, old value, new value, numeric change."
    },
    {
     "id": "fields",
     "label": "Fields",
     "description": "Per field: compared, key, only in old, only in new or ignored; types; values changed and share."
    }
   ],
   "libraries": [],
   "relatedSummary": "Use the core **Compare** block for a pass or fail test in a workflow, and this block to report what changed.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Compare",
     "page": null,
     "note": "Checks an Actual input against an Expected input: whether fields, record counts and cell values match, with allowed differences. This block lists added, removed and changed records by key, with field-level differences."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Reconciliation-Matcher",
     "name": "Reconciliation Matcher",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Reconciliation-Matcher/index.html",
     "note": "Matches the items of two ledgers on amount, reference and date, with tolerances."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Compare/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Data-Compare",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Compare/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Data-Compare/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Compare/sample-data/customers-after.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Compare/sample-data/customers-before.csv"
    ]
   }
  },
  {
   "id": "Preparation/Data-Quality-Analyser",
   "name": "Data Quality Analyser",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Finds duplicate records, missing values and numeric outliers, and outputs an issue log, the data annotated with its issues, and a copy with missing values filled.",
   "tags": [
    "quality",
    "prep",
    "verify",
    "check",
    "inspect",
    "data quality",
    "missing values",
    "duplicates",
    "outliers",
    "imputation"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "annotateData",
     "title": "Output annotated data",
     "type": "Yes / no",
     "description": "The input with a Row ID, the issues found in each record (Issue Flags) and how many there are (Issue Count)."
    },
    {
     "name": "findIssues",
     "title": "Output issue log",
     "type": "Yes / no",
     "description": "One record per issue found: Row ID, Issue Type (DUPLICATE_ROW, MISSING_VALUE or OUTLIER), Issue Field and Issue Details."
    },
    {
     "name": "enrichData",
     "title": "Output enriched data",
     "type": "Yes / no",
     "description": "The input with missing numbers filled with the field's median and missing text with its most common value, and a Was Enriched field. Dates are not filled."
    }
   ],
   "outputs": [
    {
     "id": "annotated",
     "label": "Annotated",
     "description": "The input with Row ID, Issue Flags and Issue Count. Empty unless Output annotated data is ticked."
    },
    {
     "id": "issues",
     "label": "Issues",
     "description": "One record per issue: Row ID, Issue Type, Issue Field, Issue Details. Empty unless Output issue log is ticked."
    },
    {
     "id": "enriched",
     "label": "Enriched",
     "description": "Missing numbers filled with the median, missing text with the most common value; Was Enriched marks changed records. Empty unless Output enriched data is ticked."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Validate data** block checks a dataset against rules you set and stops or warns when they fail, and **De-duplicate** removes exact duplicates rather than listing them. This block needs no rules: it looks for missing values, duplicate rows and outliers in every field at once, and the community blocks linked here each take one of those checks further.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Outlier-Flagger",
     "name": "Outlier Flagger",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Outlier-Flagger/index.html",
     "note": "Flags outliers by IQR, z-score or MAD, per field and within groups, and can cap them. This block applies one fixed IQR rule as one check among three."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Imputation",
     "name": "Imputation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Imputation/index.html",
     "note": "More ways to fill missing values: mean, median, previous value, interpolation, within groups."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Data-Profiler",
     "name": "Data Profiler",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Data-Profiler/index.html",
     "note": "One record per field with missing and distinct counts and statistics."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Validation-Rules",
     "name": "Data Validation Rules",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Validation-Rules/index.html",
     "note": "Checks the data against rules you define, rather than fixed checks."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Duplicate-Survivorship",
     "name": "Duplicate Survivorship",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Duplicate-Survivorship/index.html",
     "note": "Groups duplicates on several match keys and keeps the best record of each."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Cleansing",
     "name": "Data Cleansing",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Cleansing/index.html",
     "note": "Fixes the problems in one step: empty rows and fields, placeholders, stray characters, case."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Data-Quality-Analyser",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Quality-Analyser/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Data-Quality-Analyser/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Quality-Analyser/sample-data/branch-sales.csv"
    ]
   }
  },
  {
   "id": "Preparation/Data-Validation-Rules",
   "name": "Data Validation Rules",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Data quality",
   "description": "Validates data against a rules table or typed rules (not empty, unique, ranges, lists, patterns, emails, postcodes, field comparisons) with violations per row and per rule.",
   "tags": [
    "data validation",
    "data quality",
    "rules",
    "checks",
    "audit",
    "master data",
    "testing",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "key_fields",
     "title": "Key fields for the violations list",
     "type": "Fields",
     "description": "For example Customer ID."
    },
    {
     "name": "rule_field_field",
     "title": "Field column",
     "type": "Field",
     "description": "Column of the rules table naming the field to check."
    },
    {
     "name": "rule_check_field",
     "title": "Check column",
     "type": "Field",
     "description": "Column naming the check, such as NOT_EMPTY or BETWEEN."
    },
    {
     "name": "rule_value_field",
     "title": "Value column",
     "type": "Field",
     "description": "Column with the check's value, such as a range or list."
    },
    {
     "name": "rule_severity_field",
     "title": "Severity column",
     "type": "Field",
     "description": "Column with Error or Warning."
    },
    {
     "name": "rule_name_field",
     "title": "Rule name column",
     "type": "Field",
     "description": "Column with a readable name for each rule."
    }
   ],
   "options": [
    {
     "name": "rules",
     "title": "Rules (one per line)",
     "type": "Text",
     "description": "Field | CHECK | value | severity, for example: Email | EMAIL | | Error"
    },
    {
     "name": "examples",
     "title": "Example values per rule",
     "type": "Whole number"
    }
   ],
   "outputs": [
    {
     "id": "violations",
     "label": "Violations",
     "description": "One row per failed check: row number, key fields, rule, field, check, severity, value found."
    },
    {
     "id": "rules",
     "label": "Rules",
     "description": "Per rule: rows checked, rows failing, failure rate, status, example values, first failing row."
    },
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with Errors, Warnings, Failed Rules and Valid."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Validate data** block can stop data with errors from passing through: use it to guard a workflow. This block is for reporting, from rules in a table that business users can edit.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Validate data",
     "page": null,
     "note": "Flags schema problems, value-rule matches and record-count limits as warnings or errors, and can stop the data. This block outputs a per-record and per-rule results table from a rules table."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "Fixed checks (missing values, duplicate rows, IQR outliers) with no configuration. This block checks the rules you define."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Geo/UK-Postcode-Tools",
     "name": "UK Postcode Tools",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/UK-Postcode-Tools/index.html",
     "note": "Goes beyond a format check: standardises postcodes and adds area, district and country."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Validation-Rules/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Data-Validation-Rules",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Validation-Rules/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Data-Validation-Rules/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Validation-Rules/sample-data/customer-master.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Data-Validation-Rules/sample-data/rules.csv"
    ]
   }
  },
  {
   "id": "Preparation/Date-Range-Expander",
   "name": "Date Range Expander",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Expands each start / end date range into one row per day, week, month, quarter or year, with days in each period and amounts prorated by days.",
   "tags": [
    "generate rows",
    "date range",
    "prorate",
    "revenue recognition",
    "calendar",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Start date",
     "type": "Field",
     "description": "Date (or date text) the range starts on."
    },
    {
     "name": "end_field",
     "title": "End date",
     "type": "Field",
     "description": "Date (or date text) the range ends on."
    },
    {
     "name": "prorate_fields",
     "title": "Amounts to prorate",
     "type": "Fields",
     "description": "Number fields split across periods in proportion to the days in each period. Each gets a new field '<name> (prorated)'."
    }
   ],
   "options": [
    {
     "name": "inclusive_end",
     "title": "End date is included",
     "type": "Yes / no",
     "description": "Ticked: a range 1 to 3 January covers 3 days (contracts, absences). Unticked: it covers 2 days (hotel nights, rentals)."
    },
    {
     "name": "missing_end",
     "title": "When the end date is missing",
     "type": "Choice",
     "description": "What to do with a range that has no end date."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April (UK style). Untick for 4 March (US style). ISO dates (2025-04-03) are always read correctly."
    },
    {
     "name": "frequency",
     "title": "One row per",
     "type": "Choice",
     "description": "Size of the periods each range is split into."
    },
    {
     "name": "max_periods",
     "title": "Maximum periods per record",
     "type": "Whole number",
     "description": "Safety limit: records spanning more periods than this are skipped."
    }
   ],
   "outputs": [
    {
     "id": "expanded",
     "label": "Expanded",
     "description": "One row per record and period, with Period, Period Start, Period End, Period Number, Days In Period, Days In Range, Share Of Range and prorated amounts."
    },
    {
     "id": "skipped",
     "label": "Skipped",
     "description": "Records that could not be expanded, with the reason."
    }
   ],
   "libraries": [],
   "relatedSummary": "Expanding each contract, booking or subscription into one row per day, week or month, with amounts prorated, otherwise needs a join to a calendar and formulas; this block does it in one step.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Value sequence",
     "page": null,
     "note": "Generates one date range as a single field, not one per record."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Occupancy-From-Intervals",
     "name": "Occupancy From Intervals",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Occupancy-From-Intervals/index.html",
     "note": "Average and peak concurrency over time from start and end times, against capacity."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Date-Range-Expander/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Date-Range-Expander",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Date-Range-Expander/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Date-Range-Expander/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Date-Range-Expander/sample-data/subscription-contracts.csv"
    ]
   }
  },
  {
   "id": "Preparation/Duplicate-Survivorship",
   "name": "Duplicate Survivorship",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Data quality",
   "description": "Finds duplicates within a dataset on several match keys, keeps a survivor per group by rules and fills its gaps from the duplicates.",
   "tags": [
    "deduplication",
    "survivorship",
    "master data",
    "golden record",
    "duplicates",
    "data quality",
    "MDM",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "digits_only_fields",
     "title": "Compare on digits only",
     "type": "Fields",
     "description": "For example Phone."
    },
    {
     "name": "rule_field",
     "title": "Date or value for the rule",
     "type": "Field",
     "description": "The date or number the Keep rule looks at, such as Last Updated."
    }
   ],
   "options": [
    {
     "name": "match_keys",
     "title": "Match keys",
     "type": "Text",
     "description": "Keys separated by ';', fields within a key by '+', for example: Email; Name + Postcode"
    },
    {
     "name": "survivor_rule",
     "title": "Keep",
     "type": "Choice",
     "description": "Which record in each duplicate group is kept."
    },
    {
     "name": "fill_gaps",
     "title": "Fill gaps in the survivor from its duplicates",
     "type": "Yes / no",
     "description": "Empty fields in the kept record take a value from its duplicates."
    }
   ],
   "outputs": [
    {
     "id": "records",
     "label": "Records",
     "description": "Every record with Duplicate Group, Group Size, Survivor, Linked By and Filled Fields."
    },
    {
     "id": "survivors",
     "label": "Survivors",
     "description": "One record per group (the de-duplicated table), with records merged and fields filled."
    },
    {
     "id": "groups",
     "label": "Groups",
     "description": "Per duplicate group: records, survivor row, keys that linked them."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **De-duplicate** block is enough for exact duplicates on the fields you choose. This block matches on several alternative keys and chooses the survivor by a rule (most complete, latest or earliest date, highest or lowest value).",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "De-duplicate",
     "page": null,
     "note": "Keeps one record from each set of exact duplicates on chosen fields. This block also chooses the survivor by rules and fills its gaps from the other duplicates."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "Flags exact duplicate rows; it does not choose a survivor or fill gaps."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Fuzzy-Match",
     "name": "Fuzzy Match",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Fuzzy-Match/index.html",
     "note": "Groups near-duplicates by fuzzy text similarity, for names written differently."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Duplicate-Survivorship/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Duplicate-Survivorship",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Duplicate-Survivorship/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Duplicate-Survivorship/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Duplicate-Survivorship/sample-data/supplier-master.csv"
    ]
   }
  },
  {
   "id": "Preparation/Expand-Date-Fields",
   "name": "Expand Date Fields",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Adds year, quarter, month, week, day and day-name fields for each chosen date field, for filtering and grouping by part of a date without formulas.",
   "tags": [
    "date",
    "expand",
    "week",
    "year",
    "quarter",
    "month",
    "day of week",
    "iso week",
    "date parts"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "dateFields",
     "title": "Date fields to expand",
     "type": "Fields",
     "description": "Each chosen field gets its own set of date-part fields, named after it."
    }
   ],
   "options": [
    {
     "name": "timezoneHandling",
     "title": "Time zones",
     "type": "Choice",
     "description": "How to read dates that carry a time zone other than UTC. Omniscope passes Date fields in UTC, so for ordinary Date fields the choices give the same result."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Expanded",
     "description": "The input records with year, quarter, month, week and day fields added for each chosen date field."
    }
   ],
   "libraries": [],
   "relatedSummary": "Formulas such as `DATEUNIT` and `DATEFORMAT` take one part of a date at a time, each a formula per date field, and week numbers from a date pattern depend on the server's regional settings. This block adds eleven parts per date field at once, with English names and ISO weeks paired with their ISO year, the same on any server.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Date-Dimension",
     "name": "Date Dimension",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Date-Dimension/index.html",
     "note": "Generates a whole calendar table, with fiscal periods and public holidays, to join to your data."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Expand-Date-Fields/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Expand-Date-Fields",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Expand-Date-Fields/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Expand-Date-Fields/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Expand-Date-Fields/sample-data/orders.csv"
    ]
   }
  },
  {
   "id": "Preparation/Field-Renamer",
   "name": "Field Renamer",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Renames the fields of a data set from a list of rules (current name, new name) held in another data set, and lists the rules that matched nothing and the fields left unchanged.",
   "tags": [
    "field",
    "organiser",
    "rename",
    "name",
    "mapping",
    "schema"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "currentNames",
     "title": "Current field names",
     "type": "Field",
     "description": "The field in the rules (first input) that holds each field's current name."
    },
    {
     "name": "newNames",
     "title": "New field names",
     "type": "Field",
     "description": "The field in the rules (first input) that holds the name to give it."
    }
   ],
   "options": [],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Renamed",
     "description": "The second input with its fields renamed by the rules; values unchanged."
    },
    {
     "id": "Unused",
     "label": "Unused rules",
     "description": "Rules whose current name is not a field of the data: fields Current Name and New Name."
    },
    {
     "id": "Unchanged",
     "label": "Unchanged",
     "description": "Fields of the data that no rule renamed: one record per field, in the field Field."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Field organiser** and **Bulk field organiser** rename fields in the block's settings, which suits a handful of fixed names. This block takes the renames from a rules table on its first input, so the list can live in a spreadsheet or database and change without editing the workflow, and reports rules that matched nothing.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Streaming-Field-Renamer",
     "name": "Streaming Field Renamer",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Streaming-Field-Renamer/index.html",
     "note": "Takes the rename rules from its options or a CSV file and streams the data in chunks. Use Field Renamer when the rules come from another block, such as a database table."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Canonical-Schema-Mapper",
     "name": "Canonical Schema Mapper",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Canonical-Schema-Mapper/index.html",
     "note": "Maps several aliases to one name, combines fields and sets types, from rules kept as data."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Bulk field organiser",
     "page": null,
     "note": "Renames many fields by pattern, such as a prefix or find and replace."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Field-Renamer/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Field-Renamer",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Field-Renamer/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Field-Renamer/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Field-Renamer/sample-data/crm-export.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Field-Renamer/sample-data/rename-rules.csv"
    ]
   }
  },
  {
   "id": "Preparation/ForEach/ForEach",
   "name": "For Each (Separate Workflows)",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Workflow",
   "description": "Runs another Omniscope project once per input record, setting its project parameters from the record before each run, and lists the result of every run.",
   "tags": [
    "for each",
    "loop",
    "iteration",
    "parameter",
    "project parameters",
    "batch",
    "orchestration",
    "rest api"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Parameters",
     "title": "Parameter fields",
     "type": "Fields",
     "description": "Input fields named like parameters of the other project. Each record sets these parameters, then the workflow runs once."
    }
   ],
   "options": [
    {
     "name": "Iox_File_Url",
     "title": "Project address",
     "type": "Text",
     "description": "The address of the project to run, ending in .iox. On the same server, the internal address such as http://127.0.0.1:24679/Folder/Project.iox/ avoids the public address and its certificate. A report or workflow address of the project is cut back to the project."
    },
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a server that asks for sign-in with a list of users: sent with basic authentication. Leave empty when no sign-in is needed."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. The password for the username."
    },
    {
     "name": "Block_Names",
     "title": "Blocks to run",
     "type": "Text",
     "description": "Optional. Names of the blocks to run in the other project, separated by commas. Empty: the whole workflow. Blocks they depend on run too when needed."
    },
    {
     "name": "Refresh_From_Source",
     "title": "Refresh from source",
     "type": "Yes / no",
     "description": "Reload the data from the sources on every run, rather than reuse data the blocks already hold."
    },
    {
     "name": "ignore_errors",
     "title": "Carry on after a failed run",
     "type": "Yes / no",
     "description": "When a run fails or is cancelled, record it in the Runs output and go on with the next record. Off: stop at the first failed run."
    },
    {
     "name": "busy_wait_minutes",
     "title": "Wait for a busy project (minutes)",
     "type": "Whole number",
     "description": "If the project is already running when a run should start, or a run stays queued, wait up to this long before stopping with an error. Empty: 10."
    }
   ],
   "outputs": [
    {
     "id": "runs",
     "label": "Runs",
     "description": "One record per run: Run number, the parameter values, Result (COMPLETED, FAILED or CANCELLED) and the Job ID."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **For each** block is simpler and faster, and the right choice whenever the repeated blocks can live in the same project. This block repeats a different project through the server's Workflow REST API, waiting for each run: use it for a shared template project, or when what matters is what each run writes or publishes.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "For each",
     "page": null,
     "note": "Repeats part of the workflow it sits in and appends each run's output, with no server settings. Use this block when the work to repeat is a separate project."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/ForEach/ForEachMultiStage",
     "name": "For Each Multi Stage",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ForEachMultiStage/index.html",
     "note": "Also runs a separate project per record, but in up to three ordered stages, with an optional stage before the loop."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/ForEach/ProjectParameters",
     "name": "Project Parameters Batch Setting",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ProjectParameters/index.html",
     "note": "Sets parameters in many projects without running them."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ForEach/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/ForEach/ForEach",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEach/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/ForEach/ForEach/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEach/sample-data/branches.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEach/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Preparation/ForEach/ForEachMultiStage",
   "name": "For Each Multi Stage",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Workflow",
   "description": "Runs another Omniscope project once per input record in up to three stages, each a separate run of its own blocks, with an optional stage before the loop; sets the project's parameters from each record first.",
   "tags": [
    "for each",
    "loop",
    "iteration",
    "parameter",
    "project parameters",
    "stage",
    "orchestration",
    "rest api"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Parameters",
     "title": "Parameter fields",
     "type": "Fields",
     "description": "Input fields named like parameters of the other project. Each record sets these parameters, then runs the stages."
    }
   ],
   "options": [
    {
     "name": "Iox_File_Url",
     "title": "Project address",
     "type": "Text",
     "description": "The address of the project to run, ending in .iox. On the same server, the internal address such as http://127.0.0.1:24679/Folder/Project.iox/ avoids the public address and its certificate. A report or workflow address of the project is cut back to the project."
    },
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a server that asks for sign-in with a list of users: sent with basic authentication. Leave empty when no sign-in is needed."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. The password for the username."
    },
    {
     "name": "Block_Names_Before",
     "title": "Blocks before the loop",
     "type": "Text",
     "description": "Optional. Blocks to run once, before any parameter is set, separated by commas. Empty: no stage before the loop."
    },
    {
     "name": "Refresh_From_Source_Before",
     "title": "Refresh from source (before the loop)",
     "type": "Yes / no",
     "description": "Reload the data from the sources when running the blocks before the loop."
    },
    {
     "name": "Block_Names",
     "title": "Stage 1 blocks",
     "type": "Text",
     "description": "Optional. Blocks to run first on each pass, separated by commas. Empty: the whole workflow."
    },
    {
     "name": "Refresh_From_Source",
     "title": "Refresh from source (stage 1)",
     "type": "Yes / no",
     "description": "Reload the data from the sources in stage 1."
    },
    {
     "name": "Block_Names2",
     "title": "Stage 2 blocks",
     "type": "Text",
     "description": "Optional. Blocks to run on each pass once stage 1 has finished. Empty: no stage 2."
    },
    {
     "name": "Refresh_From_Source2",
     "title": "Refresh from source (stage 2)",
     "type": "Yes / no",
     "description": "Reload the data from the sources in stage 2."
    },
    {
     "name": "Block_Names3",
     "title": "Stage 3 blocks",
     "type": "Text",
     "description": "Optional. Blocks to run on each pass once stage 2 has finished. Empty: no stage 3."
    },
    {
     "name": "Refresh_From_Source3",
     "title": "Refresh from source (stage 3)",
     "type": "Yes / no",
     "description": "Reload the data from the sources in stage 3."
    },
    {
     "name": "ignore_errors",
     "title": "Carry on after a failed pass",
     "type": "Yes / no",
     "description": "When a stage fails or is cancelled, skip the rest of that pass, record it in the Passes output and go on with the next record. Off: stop at the first failure."
    },
    {
     "name": "busy_wait_minutes",
     "title": "Wait for a busy project (minutes)",
     "type": "Whole number",
     "description": "If the project is already running when a run should start, or a run stays queued, wait up to this long before stopping with an error. Empty: 10."
    }
   ],
   "outputs": [
    {
     "id": "passes",
     "label": "Passes",
     "description": "One record per pass: Pass number, the parameter values, Result (COMPLETED, FAILED or CANCELLED), the stage that failed, and the Job IDs of the stages."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **For each** block is the choice whenever the repeated blocks can live in the same project. This block runs a separate project per record in up to three ordered runs, for blocks that must run in an order their connections do not impose.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "For each",
     "page": null,
     "note": "Repeats blocks in the same project, appending each run's output; simpler and faster when the blocks can live there. Use this block when each record needs several ordered runs of a separate project."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/ForEach/ForEach",
     "name": "For Each (Separate Workflows)",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ForEach/index.html",
     "note": "Runs a separate project once per record in a single run. Use this block when each record needs several runs in a set order."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "sequentialworkflowexecution",
     "name": "Sequential Workflow Execution",
     "page": "https://visokio.github.io/omniscope-custom-views/sequentialworkflowexecution/index.html",
     "note": "A report button that runs blocks one after another, each as a separate run, once rather than per record."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ForEachMultiStage/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/ForEach/ForEachMultiStage",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEachMultiStage/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/ForEach/ForEachMultiStage/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEachMultiStage/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ForEachMultiStage/sample-data/regions.csv"
    ]
   }
  },
  {
   "id": "Preparation/ForEach/ProjectParameters",
   "name": "Project Parameters Batch Setting",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Workflow",
   "description": "Sets project parameters in one or more Omniscope projects: each input record gives a project's address and the values for parameters named like its fields.",
   "tags": [
    "parameter",
    "project parameters",
    "batch",
    "for each",
    "rest api",
    "orchestration"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Iox_File_Url",
     "title": "Project address field",
     "type": "Field",
     "description": "The field holding each record's project address, ending in .iox. On the same server, the internal address such as http://127.0.0.1:24679/Folder/Project.iox/ avoids the public address and its certificate. A report or workflow address of the project is cut back to the project."
    },
    {
     "name": "Parameters",
     "title": "Parameter fields",
     "type": "Fields",
     "description": "Fields named like the projects' parameters. Each record sets these parameters in its project."
    }
   ],
   "options": [
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a server that asks for sign-in with a list of users: sent with basic authentication. Leave empty when no sign-in is needed."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. The password for the username."
    }
   ],
   "outputs": [
    {
     "id": "output",
     "label": "Records",
     "description": "The input records, unchanged, once every project's parameters have been set."
    }
   ],
   "libraries": [],
   "relatedSummary": "No core block changes another project's parameters; they are set in each project's Parameters dialog or through the server's Workflow REST API, and the core **For each** block changes them only for its own loop. This block calls that API once per input record, so one table sets the parameters of many projects.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Set Project Parameters",
     "name": "Set Project Parameters",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Set%20Project%20Parameters/index.html",
     "note": "Sets several parameters of one project from name / value records. This block sets them in many projects, one record per project."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/ForEach/ForEach",
     "name": "For Each (Separate Workflows)",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ForEach/index.html",
     "note": "Sets a project's parameters and then runs it, once per record."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ProjectParameters/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/ForEach/ProjectParameters",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ProjectParameters/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/ForEach/ProjectParameters/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ProjectParameters/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/ForEach/ProjectParameters/sample-data/project-settings.csv"
    ]
   }
  },
  {
   "id": "Preparation/Geo/Centroids",
   "name": "Centroids from GeoJSON",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Geo",
   "description": "Finds a centre point (latitude and longitude) for each shape, from records with GeoJSON or WKT shapes or from a GeoJSON file or shapefile, and outputs it with the shape's ID or other fields.",
   "tags": [
    "geojson",
    "centroid",
    "shapefile",
    "polygon",
    "centre point",
    "latitude",
    "longitude",
    "map"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "geometryField",
     "title": "Geometry field",
     "type": "Field",
     "description": "The input field holding each shape as GeoJSON geometry or WKT text, in longitude / latitude (WGS 84)."
    },
    {
     "name": "inputIdField",
     "title": "ID field",
     "type": "Field",
     "description": "Optional: the input field that identifies each shape, kept beside Lat and Long. Leave empty to keep every field except the geometry."
    }
   ],
   "options": [
    {
     "name": "geojsonfile",
     "title": "Shapes file",
     "type": "File",
     "description": "Used when no geometry field is chosen. A path on the server to a GeoJSON file (.geojson, .json), a shapefile (.shp, with its .shx, .dbf and .prj files beside it, or a .zip of them) or a GeoPackage (.gpkg)."
    },
    {
     "name": "shapeIDfield",
     "title": "Shape ID property",
     "type": "Text",
     "description": "For a shapes file: the property that identifies each shape, for example \"code\" or \"name\" (case matters). Leave empty to output every property."
    },
    {
     "name": "pointType",
     "title": "Point",
     "type": "Choice",
     "description": "Centroid: the centre of the shape's area, which can fall outside a C- or L-shaped region or between the parts of a region in several pieces. Point inside the shape: a point guaranteed to be within the shape, near its middle."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Centroids",
     "description": "One record per shape: its ID (or other fields / properties) and Lat, Long of its centre point, in degrees (WGS 84)."
    }
   ],
   "libraries": [],
   "relatedSummary": "No core block turns shapes into points; this block gives one centre point per shape.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Point-In-Polygon",
     "name": "Point in Polygon",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-In-Polygon/index.html",
     "note": "Tags points with the polygon they fall in, from the same GeoJSON or WKT shapes."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Geo/Shapefile",
     "name": "Shapefile",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Shapefile/index.html",
     "note": "Finds the region each point falls in, from the same kinds of shape sources."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Centroids/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Geo/Centroids",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Centroids/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Geo/Centroids/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Centroids/sample-data/districts.csv"
    ]
   }
  },
  {
   "id": "Preparation/Geo/Gridsquare",
   "name": "Gridsquare",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Geo",
   "description": "Converts Maidenhead grid square locators (such as IO91wm) to the longitude and latitude of the square's centre.",
   "tags": [
    "gridsquare",
    "maidenhead",
    "coordinates",
    "locator",
    "latitude",
    "longitude",
    "amateur radio",
    "qth"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "gsfield",
     "title": "Grid square field",
     "type": "Field",
     "description": "The text field with the grid square locators, for example \"IO91wm\" (2 to 10 characters)."
    }
   ],
   "options": [
    {
     "name": "lonfield",
     "title": "Longitude field",
     "type": "Text",
     "description": "Name of the field to create for the longitude (X) of the square's centre, in decimal degrees. An existing field with this name is replaced."
    },
    {
     "name": "latfield",
     "title": "Latitude field",
     "type": "Text",
     "description": "Name of the field to create for the latitude (Y) of the square's centre, in decimal degrees. An existing field with this name is replaced."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Located",
     "description": "The input records with the longitude and latitude of each locator's centre added; empty where the locator is missing or not valid."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Geocode** block looks up coordinates for place names and addresses; a Maidenhead locator is not a place name but a code, converted by calculation, which this block does.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Gridsquare/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Geo/Gridsquare",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Gridsquare/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Geo/Gridsquare/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Gridsquare/sample-data/station-log.csv"
    ]
   }
  },
  {
   "id": "Preparation/Geo/Shapefile",
   "name": "Shapefile",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Geo",
   "description": "Finds which region each latitude / longitude point falls in, and adds the region's label to the point's record. The regions come from records with GeoJSON or WKT shapes on a second input, or from a shapefile or GeoJSON file.",
   "tags": [
    "shapefile",
    "geo",
    "geojson",
    "shp",
    "point in polygon",
    "regions",
    "spatial join",
    "latitude",
    "longitude"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "latitude_field",
     "title": "Latitude",
     "type": "Field",
     "description": "The input field with each point's latitude, in decimal degrees."
    },
    {
     "name": "longitude_field",
     "title": "Longitude",
     "type": "Field",
     "description": "The input field with each point's longitude, in decimal degrees."
    },
    {
     "name": "regionGeometryField",
     "title": "Region geometry field",
     "type": "Field",
     "description": "The second input's field holding each region's shape as GeoJSON geometry or WKT text, in longitude / latitude (WGS 84)."
    },
    {
     "name": "regionLabelField",
     "title": "Region label field",
     "type": "Field",
     "description": "The second input's field to put in the regions field, for example a name or code."
    }
   ],
   "options": [
    {
     "name": "shp_file",
     "title": "Shapefile",
     "type": "File",
     "description": "Used when the second input is not used. A path on the server to a shapefile (.shp, with its .shx, .dbf and .prj files beside it, or a .zip of them), a GeoJSON file (.geojson, .json) or a GeoPackage (.gpkg)."
    },
    {
     "name": "label",
     "title": "Label property",
     "type": "Text",
     "description": "For a regions file: the property to put in the regions field, for example a name or code (case matters). Run the block once with this empty to see the properties in the Regions output."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Joined",
     "description": "The input records with a regions field: the label of the region each point is in (several, comma-separated, where regions overlap); empty for points in no region."
    },
    {
     "id": "Output Data 2",
     "label": "Regions",
     "description": "One record per region: its fields or properties and its shape as text (WKT). With a file, use it to choose the label property."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Join** block matches on equal values, so it cannot tell which region a point falls in. This block reads a shapefile, GeoJSON file or GeoPackage directly, converting projected coordinate systems such as British National Grid for you.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Analytics/Point-In-Polygon",
     "name": "Point in Polygon",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Point-In-Polygon/index.html",
     "note": "Tags points with the polygon they fall in, from GeoJSON or WKT shapes on a second input, appends the polygon's fields and counts points per polygon. Use this block to read a shapefile directly."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Geo/Centroids",
     "name": "Centroids from GeoJSON",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Centroids/index.html",
     "note": "A centre point for each region's shape."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/Shapefile/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Geo/Shapefile",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Shapefile/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Geo/Shapefile/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Shapefile/sample-data/depots.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/Shapefile/sample-data/districts.csv"
    ]
   }
  },
  {
   "id": "Preparation/Geo/UK-Postcode-Tools",
   "name": "UK Postcode Tools",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Geo",
   "description": "Validates, standardises and extracts UK postcodes and splits them into area, district and sector with area name and country, offline.",
   "tags": [
    "postcode",
    "uk",
    "address",
    "validation",
    "data quality",
    "geography",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "postcode_field",
     "title": "Postcode or address field",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "extract_from_text",
     "title": "Find the postcode inside the text",
     "type": "Yes / no",
     "description": "Tick when the field is a full address rather than just a postcode."
    },
    {
     "name": "replace_field",
     "title": "Replace the field with the formatted postcode",
     "type": "Yes / no",
     "description": "Otherwise a new field is added. Ignored when extracting from text."
    },
    {
     "name": "prefix",
     "title": "Prefix for new fields",
     "type": "Text",
     "description": "New fields are '<prefix> Valid', '<prefix> Area' and so on."
    }
   ],
   "outputs": [
    {
     "id": "postcodes",
     "label": "Postcodes",
     "description": "The input with formatted postcode, validity, problem, area, district, sector, area name and country."
    },
    {
     "id": "areas",
     "label": "Areas",
     "description": "Records per country and postcode area, plus counts per problem for invalid postcodes."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block validates and standardises UK postcodes and adds their area, district and country, offline, with no paid lookup service.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Split-Address",
     "name": "Split Address",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Split-Address/index.html",
     "note": "Splits an address into street name, number and suffix."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Validation-Rules",
     "name": "Data Validation Rules",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Validation-Rules/index.html",
     "note": "Includes a UK postcode format check among many rule types."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/UK-Postcode-Tools/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Geo/UK-Postcode-Tools",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/UK-Postcode-Tools/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Geo/UK-Postcode-Tools/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Geo/UK-Postcode-Tools/sample-data/customer-addresses.csv"
    ]
   }
  },
  {
   "id": "Preparation/Imputation",
   "name": "Imputation",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Cleansing",
   "description": "Fills missing values with the mean, median, mode, a constant, previous / next value or interpolation, optionally within groups, with indicator fields and a summary.",
   "tags": [
    "imputation",
    "missing values",
    "nulls",
    "fill",
    "median",
    "interpolate",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Fields to impute",
     "type": "Fields",
     "description": "Leave empty to impute every field (except group and order fields)."
    },
    {
     "name": "group_fields",
     "title": "Within groups (optional)",
     "type": "Fields",
     "description": "Compute mean / median / mode, and fill previous / next, separately for each combination of these fields."
    },
    {
     "name": "order_fields",
     "title": "Order by (for previous / next / interpolation)",
     "type": "Fields",
     "description": "Row order used for previous value, next value and interpolation, usually a date."
    }
   ],
   "options": [
    {
     "name": "numeric_method",
     "title": "Number fields",
     "type": "Choice",
     "description": "Median is robust to outliers and is the usual default."
    },
    {
     "name": "text_method",
     "title": "Text fields",
     "type": "Choice",
     "description": "How missing text is filled."
    },
    {
     "name": "constant_number",
     "title": "Constant for numbers",
     "type": "Number"
    },
    {
     "name": "constant_text",
     "title": "Constant for text",
     "type": "Text"
    },
    {
     "name": "extra_missing",
     "title": "Also treat as missing",
     "type": "Text",
     "description": "Comma-separated sentinel values, such as -999, 9999, unknown."
    },
    {
     "name": "fallback_overall",
     "title": "Fall back to the overall value",
     "type": "Yes / no",
     "description": "When a group has no values at all (or interpolation has no neighbour on one side), use the overall statistic or the nearest value."
    },
    {
     "name": "add_flags",
     "title": "Add 'Was Imputed' indicator fields",
     "type": "Yes / no",
     "description": "Adds a Yes / No field per imputed field, so filled values stay visible."
    },
    {
     "name": "keep_integers",
     "title": "Keep whole-number fields whole",
     "type": "Yes / no",
     "description": "Round imputed values in integer fields."
    }
   ],
   "outputs": [
    {
     "id": "imputed",
     "label": "Imputed",
     "description": "The input with missing values filled, plus '<field> Was Imputed' indicators."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per field: method, missing before, imputed, still missing, value used."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block adds the mean, median, mode, a constant, the next value and interpolation to the core **Fill down** block's previous value, within groups, with a flag on every filled value.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Fill down",
     "page": null,
     "note": "Carries the previous value down into blanks, one of this block's methods."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "Finds missing values among other issues and fills them in a simple way."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Time-Series-Filler",
     "name": "Time Series Filler",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Time-Series-Filler/index.html",
     "note": "Inserts the missing periods of a time series first, then fills them."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Imputation/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Imputation",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Imputation/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Imputation/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Imputation/sample-data/property-listings.csv"
    ]
   }
  },
  {
   "id": "Preparation/Interfaces/Kedro",
   "name": "Kedro",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Interfaces",
   "description": "Runs a Kedro project's pipeline on the block's inputs: each input is saved to a named dataset of the project's catalog, and up to two datasets the pipeline produces become the block's outputs.",
   "tags": [
    "kedro",
    "workflow",
    "interface",
    "pipeline",
    "python",
    "data science"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "path",
     "title": "Kedro project folder",
     "type": "File",
     "description": "The Kedro project's top folder on the Omniscope server: the folder holding pyproject.toml."
    },
    {
     "name": "input0",
     "title": "Input dataset 1",
     "type": "Text",
     "description": "The catalog dataset that input 1 is saved to before the run, such as 'orders'. A name the catalog doesn't declare is written to data/01_raw/<name>.csv."
    },
    {
     "name": "input1",
     "title": "Input dataset 2",
     "type": "Text",
     "description": "The catalog dataset that input 2 is saved to. Leave empty when input 2 is not connected."
    },
    {
     "name": "input2",
     "title": "Input dataset 3",
     "type": "Text",
     "description": "The catalog dataset that input 3 is saved to. Leave empty when input 3 is not connected."
    },
    {
     "name": "output0",
     "title": "Output dataset 1",
     "type": "Text",
     "description": "The dataset to output as Dataset 1 after the run: a free output of the pipeline, a catalog dataset, or data/08_reporting/<name>.csv."
    },
    {
     "name": "output1",
     "title": "Output dataset 2",
     "type": "Text",
     "description": "Optional second dataset to output, as Dataset 2."
    },
    {
     "name": "pipeline",
     "title": "Pipeline",
     "type": "Text",
     "description": "The registered pipeline to run (from pipeline_registry.py). Leave empty for the default pipeline."
    },
    {
     "name": "env",
     "title": "Environment",
     "type": "Text",
     "description": "The Kedro configuration environment (a folder in conf/, such as 'local' or 'prod'). Leave empty for Kedro's default."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Dataset 1",
     "description": "The dataset named in 'Output dataset 1', read back after the Kedro run."
    },
    {
     "id": "Output Data 2",
     "label": "Dataset 2",
     "description": "The dataset named in 'Output dataset 2', if any."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Custom Python script** block can run any Python, Kedro included, but you would write the session, catalog and file handling yourself; this block does that wiring. If the pipeline's steps are simple enough to rebuild with Omniscope's own blocks (Aggregate, Join, Field organiser), do that instead, so the workflow is visible and editable block by block.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Interfaces/Kedro/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Interfaces/Kedro",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Interfaces/Kedro/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Interfaces/Kedro/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Interfaces/Kedro/sample-data/orders.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Interfaces/Kedro/sample-data/placeholder.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Interfaces/Kedro/sample-data/region-targets.csv"
    ]
   }
  },
  {
   "id": "Preparation/JSON/Expand_Fields",
   "name": "JSON Expand fields",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "JSON",
   "description": "Turns a text field holding a JSON object into one new field per key, next to the input fields; nested objects and arrays become JSON text.",
   "tags": [
    "json",
    "expand",
    "fields",
    "semi",
    "structured",
    "parse",
    "properties"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "jsonField",
     "title": "JSON field",
     "type": "Field",
     "description": "The text field holding a JSON object in each record."
    }
   ],
   "options": [
    {
     "name": "includeInput",
     "title": "Keep the input fields",
     "type": "Yes / no",
     "description": "Output the input fields followed by the new ones. Unticked, only the new fields."
    },
    {
     "name": "fieldPrefix",
     "title": "Prefix for new fields",
     "type": "Text",
     "description": "Optional. Added to the start of each new field name, for example json_ (needed when a key matches an input field name)."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Expanded",
     "description": "The input records with one new field per JSON key."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Parse text** block is the place to start for tabular data inside a cell. This block does the simpler job of turning one JSON object per record into fields beside the input, with nothing to configure.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Parse text",
     "page": null,
     "note": "Parses JSON or XML in a field into records and columns, appended with the input fields. This block only turns a JSON object's top-level keys into fields beside the input, typing each from its values."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/JSON/Normalise",
     "name": "JSON Normalise",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Normalise/index.html",
     "note": "Flattens nested objects and arrays into records and dotted fields. Use Expand fields for one flat object per record."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Airtable",
     "name": "Airtable",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Airtable/index.html",
     "note": "Airtable lists and objects arrive as JSON text that this block can expand."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Databases/MongoDB",
     "name": "MongoDB",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Databases/MongoDB/index.html",
     "note": "Nested document values arrive as JSON text that this block can expand."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "jsontree",
     "name": "JSON Tree",
     "page": "https://visokio.github.io/omniscope-custom-views/jsontree/index.html",
     "note": "Shows the JSON as an expandable tree, to see which keys it holds before expanding them."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Expand_Fields/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/JSON/Expand_Fields",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/JSON/Expand_Fields/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/JSON/Expand_Fields/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/JSON/Expand_Fields/sample-data/web-events.csv"
    ]
   }
  },
  {
   "id": "Preparation/JSON/Normalise",
   "name": "JSON Normalise",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "JSON",
   "description": "Turns JSON text into a flat table: each object becomes a record and each array item a record of its own, nested objects become fields such as customer.name, and the input fields are repeated on every record.",
   "tags": [
    "json",
    "normalise",
    "table",
    "semi",
    "structured",
    "flatten",
    "arrays"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "jsonField",
     "title": "JSON field",
     "type": "Field",
     "description": "The text field holding JSON: an object, or an array of objects, in each record."
    }
   ],
   "options": [
    {
     "name": "includeInput",
     "title": "Keep the input fields",
     "type": "Yes / no",
     "description": "Repeat the input fields on every record made from them, before the JSON fields. Unticked, only the JSON fields."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Flattened",
     "description": "One record per JSON object or array item, with nested keys as fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Parse text** block is the place to start. This block is a Python alternative for JSON, repeating the input fields on each record it creates.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Parse text",
     "page": null,
     "note": "Parses JSON or XML in each cell into tabular records, appended with the input fields; start there. This block flattens JSON only, naming nested keys with dots (Lines_options.colour)."
    },
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/JSON/Expand_Fields",
     "name": "JSON Expand fields",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Expand_Fields/index.html",
     "note": "Adds one field per top-level key of a JSON object, keeping one record per input record. Use Normalise when arrays should become records."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Connectors/Airtable",
     "name": "Airtable",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Connectors/Airtable/index.html",
     "note": "Its attachments, linked records and multiple selects arrive as JSON text."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Databases/MongoDB",
     "name": "MongoDB",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Databases/MongoDB/index.html",
     "note": "Nested objects and arrays in its documents arrive as JSON text."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "jsontree",
     "name": "JSON Tree",
     "page": "https://visokio.github.io/omniscope-custom-views/jsontree/index.html",
     "note": "Shows the JSON as an expandable tree, to see its shape before flattening it."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/JSON/Normalise/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/JSON/Normalise",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/JSON/Normalise/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/JSON/Normalise/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/JSON/Normalise/sample-data/orders-with-lines.csv"
    ]
   }
  },
  {
   "id": "Preparation/Join/As-Of-Join",
   "name": "As-Of Join",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Join",
   "description": "Joins each row to the record in force at its date (latest on or before, next, or nearest) per key, with a maximum gap and validity end dates.",
   "tags": [
    "as-of join",
    "point in time",
    "effective date",
    "exchange rates",
    "price list",
    "slowly changing dimension",
    "temporal join",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "left_date",
     "title": "Date field",
     "type": "Field",
     "description": "Date of each row in input 1, such as an invoice date."
    },
    {
     "name": "left_keys",
     "title": "Key fields (optional)",
     "type": "Fields",
     "description": "For example Currency or Product. Matched in order with the input 2 keys."
    },
    {
     "name": "right_date",
     "title": "Effective date field",
     "type": "Field",
     "description": "Date from which each input 2 record applies, such as a rate date."
    },
    {
     "name": "right_end_date",
     "title": "End date field (optional)",
     "type": "Field",
     "description": "Valid-to date, for validity ranges."
    },
    {
     "name": "right_keys",
     "title": "Key fields",
     "type": "Fields",
     "description": "Default: the same names as input 1."
    },
    {
     "name": "fields_to_add",
     "title": "Fields to bring across (optional)",
     "type": "Fields",
     "description": "Default: all except keys and dates."
    }
   ],
   "options": [
    {
     "name": "direction",
     "title": "Direction",
     "type": "Choice",
     "description": "Which reference record each row takes: the latest on or before its date, the next on or after, or the nearest."
    },
    {
     "name": "max_gap_days",
     "title": "Maximum gap (days)",
     "type": "Number",
     "description": "0 = no limit."
    },
    {
     "name": "suffix",
     "title": "Suffix for clashing names",
     "type": "Text"
    }
   ],
   "outputs": [
    {
     "id": "joined",
     "label": "Joined",
     "description": "Input 1 with the fields from input 2, As Of Date, Days From As Of Date and As Of Status."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Rows per status, with the average and largest gap for matched rows."
    }
   ],
   "libraries": [],
   "relatedSummary": "**Inequality Join** and **Interval Join** return every match; an as-of join keeps exactly one, the record in force at each row's date, per key, with a maximum age. Doing that with a core **Join** needs a cross join and then filtering, which grows very large on big tables.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Inequality-Join",
     "name": "Inequality Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Inequality-Join/index.html",
     "note": "Joins on <, <=, >, >= conditions and returns every match. An as-of join keeps only the latest (or next, or nearest) match per row."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Interval-Join",
     "name": "Interval Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Interval-Join/index.html",
     "note": "Joins values to the intervals (start and end) that contain them."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Inflation-Adjustment",
     "name": "Inflation Adjustment",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Inflation-Adjustment/index.html",
     "note": "Uses the index in force at each date to restate money values in real terms."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/As-Of-Join/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Join/As-Of-Join",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/As-Of-Join/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Join/As-Of-Join/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/As-Of-Join/sample-data/fx-rates.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/As-Of-Join/sample-data/invoices.csv"
    ]
   }
  },
  {
   "id": "Preparation/Join/Fuzzy-Join",
   "name": "Fuzzy Terms Join",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Join",
   "description": "Joins two inputs on text fields that are written differently but share words, such as company names or product descriptions. Each record is paired with the record on the other side that shares the most distinctive words.",
   "tags": [
    "fuzzy",
    "join",
    "text matching",
    "name matching",
    "tf-idf"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "left",
     "title": "Left field",
     "type": "Field",
     "description": "The text field to match on in the first (left) input."
    },
    {
     "name": "right",
     "title": "Right field",
     "type": "Field",
     "description": "The text field to match on in the second (right) input."
    }
   ],
   "options": [
    {
     "name": "joinType",
     "title": "Join type",
     "type": "Choice",
     "description": "Which side's records each get a best match. Left: every first-input record that shares a word with the second input. Right: the same the other way round. Left + Right: both, one after the other (a pair found from both sides appears twice)."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Joined",
     "description": "Each matched pair, with the fields of both inputs. A field name found in both inputs gets .x and .y: .x is the first input in Left and Left + Right joins, the second input in a Right join. Spaces in field names become dots."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Join** block needs equal key values, and the `LEVENSHTEIN` formula compares one pair at a time. This block pairs records by the words they share, weighting distinctive words above common ones, with no threshold to tune, and suits teams where R is preferred.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Join/Fuzzy-Match",
     "name": "Fuzzy Match",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Fuzzy-Match/index.html",
     "note": "Scores character-level similarity, so it catches typos and abbreviations, and can also find near-duplicates within one input. Use this block when names share the same words in a different order or form."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Fuzzy-Join/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Join/Fuzzy-Join",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Join/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Join/Fuzzy-Join/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Join/sample-data/supplier-invoices.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Join/sample-data/supplier-master.csv"
    ]
   }
  },
  {
   "id": "Preparation/Join/Fuzzy-Match",
   "name": "Fuzzy Match",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Join",
   "description": "Finds near-duplicate records within one input (with group IDs), or the best matching record in a second input, using fuzzy text similarity.",
   "tags": [
    "fuzzy",
    "match",
    "de-duplicate",
    "record linkage",
    "join",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "match_field",
     "title": "Match field",
     "type": "Field",
     "description": "The text field to compare, such as a company or customer name."
    },
    {
     "name": "reference_field",
     "title": "Match field in second input",
     "type": "Field",
     "description": "Lookup mode only. Leave empty to use the field with the same name as the match field."
    },
    {
     "name": "second_field",
     "title": "Second field to compare (optional)",
     "type": "Field",
     "description": "An extra field compared with the same measure, such as an address line. Blended into the score using the weight below."
    },
    {
     "name": "second_reference_field",
     "title": "Second field in second input",
     "type": "Field",
     "description": "Lookup mode only. Leave empty to use the field with the same name."
    },
    {
     "name": "block_field",
     "title": "Must match exactly (optional)",
     "type": "Field",
     "description": "Only records with the same value in this field are compared, such as postcode or country. Improves precision and speeds up large inputs."
    },
    {
     "name": "block_reference_field",
     "title": "Must match exactly, second input",
     "type": "Field",
     "description": "Lookup mode only. Leave empty to use the field with the same name."
    }
   ],
   "options": [
    {
     "name": "second_weight",
     "title": "Weight of second field",
     "type": "Number",
     "description": "Share of the score that comes from the second field, from 0 to 1."
    },
    {
     "name": "algorithm",
     "title": "Similarity measure",
     "type": "Choice",
     "description": "How the similarity of two values is scored, from 0 (nothing in common) to 1 (identical after clean-up)."
    },
    {
     "name": "threshold",
     "title": "Match threshold",
     "type": "Number",
     "description": "Pairs scoring at or above this value count as a match. 0.85 to 0.92 suits company names."
    },
    {
     "name": "max_matches",
     "title": "Matches per record (lookup mode)",
     "type": "Whole number",
     "description": "How many of the best matches to return for each record of the first input."
    },
    {
     "name": "normalise",
     "title": "Ignore case, accents and punctuation",
     "type": "Yes / no",
     "description": "Compare \"Café & Co.\" and \"cafe and co\" as the same text."
    },
    {
     "name": "ignore_suffixes",
     "title": "Ignore company suffixes",
     "type": "Yes / no",
     "description": "Drop words such as Ltd, Limited, plc, LLP, Inc, Corp, Co, GmbH and The before comparing."
    },
    {
     "name": "ignore_words",
     "title": "Other words to ignore",
     "type": "Text",
     "description": "Comma-separated words to drop before comparing, such as services, group."
    }
   ],
   "outputs": [
    {
     "id": "matched",
     "label": "Matched",
     "description": "De-duplicate: the input with Group ID, Group Size, Is Group Master and Best Match Score. Lookup: the first input with the best match from the second input appended."
    },
    {
     "id": "pairs",
     "label": "Pairs",
     "description": "Every pair scoring at or above the threshold, with both values and the score."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **De-duplicate** block finds exact duplicates only, and the `LEVENSHTEIN` formula compares two values in one row. This block groups near-duplicates within one input and finds the best match for each record in a second input.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Join/Fuzzy-Join",
     "name": "Fuzzy Terms Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Fuzzy-Join/index.html",
     "note": "Joins two inputs by how many terms their text fields share. Overlaps with this block's two-input lookup mode; this block also finds near-duplicates within one input and scores with character-level similarity."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "De-duplicate",
     "page": null,
     "note": "Exact duplicates only."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Duplicate-Survivorship",
     "name": "Duplicate Survivorship",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Duplicate-Survivorship/index.html",
     "note": "Keeps the best record of each duplicate group and fills its gaps from the others."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Fuzzy-Match/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Join/Fuzzy-Match",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Match/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Join/Fuzzy-Match/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Match/sample-data/crm-accounts.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Match/sample-data/invoices-to-match.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Fuzzy-Match/sample-data/supplier-master.csv"
    ]
   }
  },
  {
   "id": "Preparation/Join/Inequality-Join",
   "name": "Inequality Join",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Join",
   "description": "Joins the first and second inputs where a number or date field of the first is less than, greater than or equal to a field of the second (<, <=, >, >=, ==), optionally only where another field is equal as well. Every matching pair is output.",
   "tags": [
    "inequality",
    "join",
    "non-equi join",
    "range join",
    "less than",
    "greater than",
    "data.table"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "left",
     "title": "Left field",
     "type": "Field",
     "description": "The number or date field of the first input to compare."
    },
    {
     "name": "leftKey",
     "title": "Equal field",
     "type": "Field",
     "description": "Optional: a field of the first input that must also equal the second input's Equal field, for example a customer or product code."
    },
    {
     "name": "right",
     "title": "Right field",
     "type": "Field",
     "description": "The number or date field of the second input to compare with."
    },
    {
     "name": "rightKey",
     "title": "Equal field",
     "type": "Field",
     "description": "Optional: the field of the second input that must equal the first input's Equal field. Choose both or neither."
    }
   ],
   "options": [
    {
     "name": "comparator",
     "title": "Comparator",
     "type": "Choice",
     "description": "How the left field (first input) must compare with the right field (second input) for two records to be joined."
    },
    {
     "name": "joinType",
     "title": "Join type",
     "type": "Choice",
     "description": "Inner: only first-input records with at least one match. Left: every first-input record; those with no match have empty second-input fields."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Joined",
     "description": "Every pair of records that satisfies the comparison (and the equal fields): first input's fields, then the second's (a name in both gets .1). Left join: unmatched first-input records too, with empty second-input fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Join** block pairs records whose keys are equal. This block joins on a single comparison, which suits \"everything before\" or \"everything above\", and keeps every matching pair, for example to count or compare them afterwards.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/As-Of-Join",
     "name": "As-Of Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/As-Of-Join/index.html",
     "note": "Keeps only the latest earlier record per row, such as the rate in force at a date."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Interval-Join",
     "name": "Interval Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Interval-Join/index.html",
     "note": "Matches a value to ranges with a start and an end, in one step."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Inequality-Join/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Join/Inequality-Join",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Inequality-Join/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Join/Inequality-Join/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Inequality-Join/sample-data/campaigns.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Inequality-Join/sample-data/orders.csv"
    ]
   }
  },
  {
   "id": "Preparation/Join/Interval-Join",
   "name": "Interval Join",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Join",
   "description": "Joins each record of the first input to the intervals in the second input that contain its value (a number or a date between a start and an end), optionally only where another field matches as well.",
   "tags": [
    "inequality",
    "interval",
    "join",
    "range join",
    "between",
    "bands",
    "lookup"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "value",
     "title": "Value in interval (first input)",
     "type": "Field",
     "description": "The number or date field of the first input to look up in the intervals."
    },
    {
     "name": "start",
     "title": "Interval start (second input)",
     "type": "Field",
     "description": "The field of the second input where each interval starts (number or date, the same kind as the value). Empty means no lower limit."
    },
    {
     "name": "end",
     "title": "Interval end (second input)",
     "type": "Field",
     "description": "The field of the second input where each interval ends (number or date). Empty means no upper limit."
    },
    {
     "name": "left",
     "title": "Field in first input",
     "type": "Field",
     "description": "A field that must also be equal, for example a zone or product code. Choose it in both inputs, or leave both empty."
    },
    {
     "name": "right",
     "title": "Field in second input",
     "type": "Field",
     "description": "The field of the second input to compare with the first input's field."
    }
   ],
   "options": [
    {
     "name": "join_type",
     "title": "Join type",
     "type": "Choice",
     "description": "Inner: only records whose value is in an interval. Left: every record of the first input; those in no interval have empty fields from the second input."
    },
    {
     "name": "start_comparator",
     "title": "Start",
     "type": "Choice",
     "description": "Whether a value equal to the interval's start is inside it. Inclusive: a value of 5 is in the interval 5 to 10. Exclusive: it is not."
    },
    {
     "name": "end_comparator",
     "title": "End",
     "type": "Choice",
     "description": "Whether a value equal to the interval's end is inside it. Inclusive: a value of 10 is in the interval 5 to 10. Exclusive: it is not, so bands such as 5 to 10 and 10 to 20 do not overlap."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Joined",
     "description": "Each first-input record once per interval that contains its value: its fields, then the interval's fields (a name in both gets .1). Left join: records in no interval are kept with empty interval fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "With the core **Join** block, a range lookup means joining every record to every band and filtering, which grows very large. This block tests both ends of an interval at once, with open-ended bands and a choice of whether each boundary counts.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/As-Of-Join",
     "name": "As-Of Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/As-Of-Join/index.html",
     "note": "Keeps the single record in force at each date (latest on or before, next or nearest), per key."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Join/Inequality-Join",
     "name": "Inequality Join",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Inequality-Join/index.html",
     "note": "Joins on a single comparison (<, <=, >, >=) and outputs every matching pair."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Tiered-Calculation",
     "name": "Tiered Calculation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Tiered-Calculation/index.html",
     "note": "Applies tier tables to amounts, charging each slice at its own rate."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Join/Interval-Join/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Join/Interval-Join",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Interval-Join/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Join/Interval-Join/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Interval-Join/sample-data/parcels.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Join/Interval-Join/sample-data/tariff.csv"
    ]
   }
  },
  {
   "id": "Preparation/Markdown-to-HTML",
   "name": "Markdown to HTML",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Converts Markdown text in a field (bold, lists, links, headings, tables) to HTML in a new field, keeping the other fields.",
   "tags": [
    "markdown",
    "html",
    "text",
    "formatting",
    "convert"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "MarkdownText",
     "title": "Markdown field",
     "type": "Field",
     "description": "The text field holding Markdown."
    }
   ],
   "options": [
    {
     "name": "outputField",
     "title": "HTML field name",
     "type": "Text",
     "description": "Name of the new field holding the HTML. Use the Markdown field's name to replace it."
    },
    {
     "name": "includeInput",
     "title": "Keep the input fields",
     "type": "Yes / no",
     "description": "Output the input fields with the HTML field after them. Unticked, only the HTML field."
    },
    {
     "name": "extensions",
     "title": "Tables, fenced code and footnotes",
     "type": "Yes / no",
     "description": "Also convert tables, fenced code blocks, footnotes, abbreviations, definition lists and attribute lists (the 'extra' extension). Unticked: standard Markdown only."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "HTML",
     "description": "The input records with the Markdown converted to HTML in a new field."
    }
   ],
   "libraries": [],
   "relatedSummary": "None of the core preparation blocks converts Markdown, and doing it with text replacements breaks down at lists, links and tables. This block converts the whole text with a standard Markdown library.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "customhtml",
     "name": "Custom HTML",
     "page": "https://visokio.github.io/omniscope-custom-views/customhtml/index.html",
     "note": "Shows HTML held in a field, such as this block's output, for the filtered or selected record or as one card per value."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Markdown-to-HTML/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Markdown-to-HTML",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Markdown-to-HTML/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Markdown-to-HTML/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Markdown-to-HTML/sample-data/product-descriptions.csv"
    ]
   }
  },
  {
   "id": "Preparation/Multi-Field-Formula",
   "name": "Multi-Field Formula",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Formula",
   "description": "Applies one operation (round, scale, share of row or column total, index to base, z-score, fill, text case...) to many fields chosen by list, name pattern or type, with optional row statistics.",
   "tags": [
    "multi-field",
    "formula",
    "bulk",
    "wide data",
    "percent of total",
    "index",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Fields",
     "type": "Fields",
     "description": "Leave empty to use a name pattern or all fields."
    },
    {
     "name": "base_field",
     "title": "Base field (index / difference)",
     "type": "Field"
    }
   ],
   "options": [
    {
     "name": "field_pattern",
     "title": "Field name pattern (regular expression)",
     "type": "Text",
     "description": "Adds every field whose name matches, for example 2025$ for all 2025 columns."
    },
    {
     "name": "type_filter",
     "title": "Only fields of type",
     "type": "Choice"
    },
    {
     "name": "operation",
     "title": "Operation",
     "type": "Choice",
     "description": "What to do to every chosen field."
    },
    {
     "name": "value",
     "title": "Value",
     "type": "Number",
     "description": "For multiply, divide, add, subtract, floor, cap and fill."
    },
    {
     "name": "decimals",
     "title": "Decimal places (round)",
     "type": "Whole number"
    },
    {
     "name": "text_value",
     "title": "Text (prefix, suffix, fill)",
     "type": "Text"
    },
    {
     "name": "output",
     "title": "Results",
     "type": "Choice",
     "description": "Overwrite the fields, or add new ones next to them."
    },
    {
     "name": "suffix",
     "title": "Suffix for new fields",
     "type": "Text",
     "description": "Added to the name of each new field."
    },
    {
     "name": "row_sum",
     "title": "Row sum",
     "type": "Yes / no",
     "description": "Adds the total of the chosen fields for each row."
    },
    {
     "name": "row_mean",
     "title": "Row mean",
     "type": "Yes / no"
    },
    {
     "name": "row_min",
     "title": "Row minimum",
     "type": "Yes / no"
    },
    {
     "name": "row_max",
     "title": "Row maximum",
     "type": "Yes / no"
    },
    {
     "name": "row_count",
     "title": "Row count of values",
     "type": "Yes / no"
    },
    {
     "name": "row_std",
     "title": "Row standard deviation",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Result",
     "description": "The input with the operation applied (or new fields added) and any row statistics."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block applies any of a range of calculations (round, scale, share of row or column total, index to a base, z-score, fill, text case) to many fields at once.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Standardisation/Standardise",
     "name": "Standardise",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Standardisation/Standardise/index.html",
     "note": "Rescales selected fields to the range 0 to 1, one of the operations this block offers."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Normalise",
     "page": null,
     "note": "Converts numeric fields to z-scores, one of this block's operations."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Bulk field organiser",
     "page": null,
     "note": "Applies the same name and type changes to many fields."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Multi-Row-Formula",
     "name": "Multi-Row Formula",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Row-Formula/index.html",
     "note": "Calculations across rows rather than across fields: previous value, running total, rolling mean."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Field-Formula/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Multi-Field-Formula",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Multi-Field-Formula/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Multi-Field-Formula/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Multi-Field-Formula/sample-data/monthly-budget.csv"
    ]
   }
  },
  {
   "id": "Preparation/Multi-Row-Formula",
   "name": "Multi-Row Formula",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Formula",
   "description": "Window calculations across rows within groups: previous / next value, change, % change, running total, rolling mean / sum / min / max over rows or days, rank and share of group.",
   "tags": [
    "multi-row",
    "lag",
    "lead",
    "running total",
    "rolling",
    "moving average",
    "window",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "value_fields",
     "title": "Value fields",
     "type": "Fields",
     "description": "Fields to calculate on. Previous / next work for any field; the other calculations need numbers."
    },
    {
     "name": "group_fields",
     "title": "Group by (optional)",
     "type": "Fields",
     "description": "Calculations restart for each combination of these fields, for example Store or Customer."
    },
    {
     "name": "sort_fields",
     "title": "Order by",
     "type": "Fields",
     "description": "Order of rows within each group, usually a date. For rolling windows in days choose exactly one date field."
    }
   ],
   "options": [
    {
     "name": "calc_previous",
     "title": "Previous value",
     "type": "Yes / no"
    },
    {
     "name": "calc_next",
     "title": "Next value",
     "type": "Yes / no"
    },
    {
     "name": "calc_change",
     "title": "Change vs previous",
     "type": "Yes / no"
    },
    {
     "name": "calc_pct_change",
     "title": "% change vs previous",
     "type": "Yes / no",
     "description": "As a fraction: 0.05 means up 5%."
    },
    {
     "name": "calc_running_total",
     "title": "Running total",
     "type": "Yes / no",
     "description": "Cumulative sum within each group."
    },
    {
     "name": "calc_rolling_mean",
     "title": "Rolling mean",
     "type": "Yes / no",
     "description": "Average over the rolling window."
    },
    {
     "name": "calc_rolling_sum",
     "title": "Rolling sum",
     "type": "Yes / no"
    },
    {
     "name": "calc_rolling_min",
     "title": "Rolling minimum",
     "type": "Yes / no"
    },
    {
     "name": "calc_rolling_max",
     "title": "Rolling maximum",
     "type": "Yes / no"
    },
    {
     "name": "calc_rank",
     "title": "Rank in group",
     "type": "Yes / no",
     "description": "1 = highest value in the group; ties share the lower rank."
    },
    {
     "name": "calc_share",
     "title": "Share of group total",
     "type": "Yes / no"
    },
    {
     "name": "calc_row_number",
     "title": "Row number in group",
     "type": "Yes / no"
    },
    {
     "name": "offset",
     "title": "Rows back / forward",
     "type": "Whole number",
     "description": "For previous, next and change: 1 compares with the row just before; 7 with daily data compares with the same weekday last week."
    },
    {
     "name": "window",
     "title": "Rolling window size",
     "type": "Whole number",
     "description": "Rows or days in each rolling window."
    },
    {
     "name": "window_unit",
     "title": "Rolling window counts",
     "type": "Choice",
     "description": "Whether the window counts rows or days of the date in Order by."
    },
    {
     "name": "full_window",
     "title": "Only when the window is full",
     "type": "Yes / no",
     "description": "Leave rolling values empty until a full window of rows (or days) is available."
    },
    {
     "name": "descending",
     "title": "Order descending",
     "type": "Yes / no",
     "description": "Tick to process newest first."
    },
    {
     "name": "keep_order",
     "title": "Keep input row order",
     "type": "Yes / no",
     "description": "Untick to output rows sorted by group and order fields."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Result",
     "description": "The input with the new calculated fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "Formulas can do much of this, since RUNNINGTOTAL and SUBSET formulas give running totals, previous values and moving averages per group, but each calculation is a formula to write and check. This block offers previous and next values, change, percentage change, running totals, rolling statistics and ranks within groups as options.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Rolling-Time-Window",
     "name": "Rolling Time Window",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Rolling-Time-Window/index.html",
     "note": "Windows of a fixed length of time (last 24 hours, 30 days) rather than a number of rows."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Running-Balance",
     "name": "Running Balance",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Running-Balance/index.html",
     "note": "Running balances per account with lowest balance and days below zero."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Multi-Field-Formula",
     "name": "Multi-Field Formula",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Field-Formula/index.html",
     "note": "One operation across many fields of the same row."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "runningpivot",
     "name": "Running Pivot View",
     "page": "https://visokio.github.io/omniscope-custom-views/runningpivot/index.html",
     "note": "Shows running totals in a pivot, by row and column, without adding fields."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Row-Formula/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Multi-Row-Formula",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Multi-Row-Formula/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Multi-Row-Formula/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Multi-Row-Formula/sample-data/store-daily-sales.csv"
    ]
   }
  },
  {
   "id": "Preparation/Name-Parser",
   "name": "Name Parser",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Parse",
   "description": "Splits full names into title, first, middle, last and suffix, tidies capitalisation, flags organisations and builds salutations.",
   "tags": [
    "name parsing",
    "salutation",
    "data cleansing",
    "contacts",
    "CRM",
    "mailing",
    "deduplication",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "name_field",
     "title": "Full name field",
     "type": "Field",
     "description": "The whole name as one piece of text."
    }
   ],
   "options": [
    {
     "name": "fix_case",
     "title": "Tidy capitalisation",
     "type": "Yes / no",
     "description": "Corrects all-capital or all-lower-case names, keeping forms such as McDonald and O'Brien."
    },
    {
     "name": "salutation_style",
     "title": "Salutation",
     "type": "Choice",
     "description": "Title and surname, or first name only."
    },
    {
     "name": "greeting",
     "title": "Greeting",
     "type": "Text"
    },
    {
     "name": "prefix",
     "title": "Prefix for new fields (optional)",
     "type": "Text"
    }
   ],
   "outputs": [
    {
     "id": "parsed",
     "label": "Parsed",
     "description": "The input with Name Type, Title, First Name, Middle Names, Last Name, Suffix, Initials, Tidy Name, Salutation, Parse Note."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Split values** and **Normalise case** blocks can split on spaces and fix capitals, but cannot tell titles, middle names, suffixes, \"Last, First\" order or organisation names apart; this block does.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Split values",
     "page": null,
     "note": "Splits a field on a separator."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Normalise case",
     "page": null,
     "note": "Changes text case."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Name-Parser/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Name-Parser",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Name-Parser/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Name-Parser/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Name-Parser/sample-data/contacts.csv"
    ]
   }
  },
  {
   "id": "Preparation/Outlier-Flagger",
   "name": "Outlier Flagger",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Data quality",
   "description": "Flags low and high outliers in many numeric fields at once, within groups, by IQR, z-score or robust MAD, with bounds and optional capping.",
   "tags": [
    "outliers",
    "IQR",
    "z-score",
    "MAD",
    "winsorise",
    "data quality",
    "anomalies",
    "audit",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields",
     "title": "Numeric fields",
     "type": "Fields",
     "description": "Leave empty to check every numeric field."
    },
    {
     "name": "group_fields",
     "title": "Within groups (optional)",
     "type": "Fields",
     "description": "For example Category."
    },
    {
     "name": "key_fields",
     "title": "Key fields for the outlier list",
     "type": "Fields",
     "description": "For example Claim ID."
    }
   ],
   "options": [
    {
     "name": "method",
     "title": "Method",
     "type": "Choice",
     "description": "IQR and MAD resist the outliers they look for; z-score suits roughly normal data."
    },
    {
     "name": "k",
     "title": "Multiplier",
     "type": "Number",
     "description": "0 = the method's default (1.5, 3 or 3.5)."
    },
    {
     "name": "min_group_size",
     "title": "Smallest group to test",
     "type": "Whole number",
     "description": "Groups with fewer values are not tested."
    },
    {
     "name": "add_capped",
     "title": "Add capped (winsorised) fields",
     "type": "Yes / no"
    }
   ],
   "outputs": [
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with an outlier flag per field, Outlier Count and Outlier Fields (and capped fields if chosen)."
    },
    {
     "id": "outliers",
     "label": "Outliers",
     "description": "One row per outlying value: row number, keys, group, field, value, direction, bound, score."
    },
    {
     "id": "bounds",
     "label": "Bounds",
     "description": "Per field and group: values, bounds, low and high counts, share, minimum, median, maximum."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Anomaly Detection** block gives each record one anomaly score, which is good for finding unusual records overall. This block applies a stated rule per field, within groups, reports the bounds used and can cap values, so the result is easy to explain.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Data-Quality-Analyser",
     "name": "Data Quality Analyser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Quality-Analyser/index.html",
     "note": "Flags IQR outliers (as well as missing values and duplicate rows) in an issue log. This block adds z-score and MAD methods, grouping, bounds per field and capping."
    },
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Anomaly Detection",
     "page": null,
     "note": "Assigns an anomaly score to each record from the selected fields, optionally within groups. This block applies explicit IQR, z-score or MAD bounds per field and can cap values."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Data-Profiler",
     "name": "Data Profiler",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Data-Profiler/index.html",
     "note": "Field statistics plus a z-score anomaly flag (3 or more standard deviations) on each record."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Time-Series-Anomalies",
     "name": "Time Series Anomaly Detection",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Time-Series-Anomalies/index.html",
     "note": "Spikes and dips in a series against a rolling baseline, allowing for weekdays."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Outlier-Flagger/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Outlier-Flagger",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Outlier-Flagger/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Outlier-Flagger/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Outlier-Flagger/sample-data/expense-claims.csv"
    ]
   }
  },
  {
   "id": "Preparation/Partition",
   "name": "Partition",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Partition",
   "description": "Numbers records in consecutive groups of a chosen size (1 for the first 1,000 records, 2 for the next 1,000 and so on) in a new Partition field, for processing or sending data in batches.",
   "tags": [
    "partition",
    "division",
    "grouping",
    "batch",
    "chunk",
    "paging"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "partition_size",
     "title": "Number of records per partition",
     "type": "Whole number",
     "description": "How many records go in each partition. The last partition holds the records left over."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Partitioned",
     "description": "The input records, in their order, with a Partition field: 1 for the first group of records, 2 for the next, and so on."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Split Data** block divides records between outputs, such as a random 30% and the rest, rather than into numbered batches of a fixed size; an **Add row ID** block and a formula can give batch numbers in two steps. This block numbers the batches directly.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Tile",
     "name": "Tile",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Tile/index.html",
     "note": "Bands records by the values of a numeric field, such as deciles."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Partition/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Partition",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Partition/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Partition/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Partition/sample-data/mailing-list.csv"
    ]
   }
  },
  {
   "id": "Preparation/Pivot/Melt-De-pivot",
   "name": "Melt De-pivot",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Pivot",
   "description": "Keeps the chosen fixed fields and turns every other field into records: one record per input record and field, with the field's name and its value. Values of different types become text.",
   "tags": [
    "pivot",
    "depivot",
    "de-pivot",
    "melt",
    "unpivot",
    "wide to long",
    "variable",
    "fixed"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fixed_fields",
     "title": "Fixed fields",
     "type": "Fields",
     "description": "Fields kept on every output record (for example an ID or name). Every other field is de-pivoted. Leave empty to de-pivot all fields."
    }
   ],
   "options": [
    {
     "name": "var_name",
     "title": "Name field",
     "type": "Text",
     "description": "Name of the new field that holds the de-pivoted field's name."
    },
    {
     "name": "value_name",
     "title": "Value field",
     "type": "Text",
     "description": "Name of the new field that holds the de-pivoted field's value."
    },
    {
     "name": "drop_empty",
     "title": "Leave out empty values",
     "type": "Yes / no",
     "description": "Skip output records whose value is empty, for example months with no sales in a sparse table."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "De-pivoted",
     "description": "One record per input record and de-pivoted field: the fixed fields, then the field's name and its value."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **De-pivot** block is the first choice for most tables. This block differs in three ways: choose the fields to keep and everything else is de-pivoted, values of different types are turned into text consistently (and the block says so), and empty values can be left out.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "De-pivot",
     "page": null,
     "note": "Does the same job with rule-based field selection and no Python. Use this block for its handling of mixed types and empty values, or as a script to adapt."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Pivot/Melt-De-pivot/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Pivot/Melt-De-pivot",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Pivot/Melt-De-pivot/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Pivot/Melt-De-pivot/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Pivot/Melt-De-pivot/sample-data/store-sales-wide.csv"
    ]
   }
  },
  {
   "id": "Preparation/RegEx-Parse",
   "name": "RegEx Parse",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Parse",
   "description": "Applies a regular expression to a text field: parse capture groups into fields, replace with back-references, tokenise matches into columns or rows, or flag matches.",
   "tags": [
    "regex",
    "regular expression",
    "parse",
    "extract",
    "tokenise",
    "replace",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "field",
     "title": "Text field",
     "type": "Field",
     "description": "The text to apply the regular expression to."
    }
   ],
   "options": [
    {
     "name": "mode",
     "title": "Mode",
     "type": "Choice",
     "description": "What to do with each match."
    },
    {
     "name": "pattern",
     "title": "Regular expression",
     "type": "Text",
     "description": "For example Order #(?P<Order>[A-Z]-\\d+) or [\\w.+-]+@[\\w-]+\\.[\\w.]+"
    },
    {
     "name": "ignore_case",
     "title": "Ignore case",
     "type": "Yes / no",
     "description": "Upper and lower case match each other."
    },
    {
     "name": "output_names",
     "title": "Output field names",
     "type": "Text",
     "description": "Comma-separated, in group (or token) order. Optional when the groups are named."
    },
    {
     "name": "tokenise_to",
     "title": "Tokenise into",
     "type": "Choice"
    },
    {
     "name": "max_columns",
     "title": "Maximum token columns",
     "type": "Whole number"
    },
    {
     "name": "keep_unmatched",
     "title": "Keep rows with no tokens (rows mode)",
     "type": "Yes / no"
    },
    {
     "name": "replacement",
     "title": "Replacement text",
     "type": "Text",
     "description": "Use \\1 or $1 for the first capture group."
    },
    {
     "name": "match_type",
     "title": "Match",
     "type": "Choice"
    },
    {
     "name": "output_field",
     "title": "Output field name (replace / match / rows)",
     "type": "Text",
     "description": "Replace mode: leave empty to overwrite the text field."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Result",
     "description": "The input with parsed, replaced, tokenised or match fields."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Search / replace** block can replace with regular expressions; this block also parses capture groups into new fields and tokenises the matches of a pattern into columns or rows.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Unstack-rows",
     "name": "Unstack Records",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Unstack-rows/index.html",
     "note": "Splits stacked values on a fixed separator into rows; this block can tokenise matches of a pattern into rows."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Search / replace",
     "page": null,
     "note": "Regular expression replacement."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Split values",
     "page": null,
     "note": "Splits a field on a separator or at fixed positions."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/RegEx-Parse/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/RegEx-Parse",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/RegEx-Parse/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/RegEx-Parse/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/RegEx-Parse/sample-data/order-notes.csv"
    ]
   }
  },
  {
   "id": "Preparation/Rolling-Time-Window",
   "name": "Rolling Time Window",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Window",
   "description": "Trailing time-window aggregates per key (count, sum, mean, min, max, distinct) for several windows, such as transactions per card in the last 24 hours.",
   "tags": [
    "rolling window",
    "time window",
    "velocity",
    "fraud features",
    "moving aggregate",
    "feature engineering",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "key_field",
     "title": "Key field",
     "type": "Field",
     "description": "For example Card or Customer."
    },
    {
     "name": "time_field",
     "title": "Timestamp field",
     "type": "Field",
     "description": "Date and time of each event."
    },
    {
     "name": "value_field",
     "title": "Value field (optional)",
     "type": "Field",
     "description": "The amount to sum, average and take the minimum and maximum of."
    },
    {
     "name": "distinct_field",
     "title": "Count distinct (optional)",
     "type": "Field",
     "description": "For example Merchant."
    }
   ],
   "options": [
    {
     "name": "windows",
     "title": "Windows",
     "type": "Text",
     "description": "For example: 30min, 1h, 24h, 7d, 4w"
    },
    {
     "name": "include_current",
     "title": "Include the current row",
     "type": "Yes / no",
     "description": "Unticked, each window covers only earlier rows: useful for scoring a new transaction against its history."
    }
   ],
   "outputs": [
    {
     "id": "rows",
     "label": "Rows",
     "description": "The input with count, sum, mean, min, max and distinct per window."
    }
   ],
   "libraries": [],
   "relatedSummary": "This block is built for velocity checks, as in card fraud rules: several trailing time windows at once (such as 1 hour, 24 hours and 7 days) per key, with counts, sums and distinct values.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Multi-Row-Formula",
     "name": "Multi-Row Formula",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Row-Formula/index.html",
     "note": "Rolling calculations over a fixed number of rows, plus previous value, change and rank."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Rolling-Time-Window/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Rolling-Time-Window",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Rolling-Time-Window/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Rolling-Time-Window/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Rolling-Time-Window/sample-data/card-transactions.csv"
    ]
   }
  },
  {
   "id": "Preparation/Sequence-Gap-Finder",
   "name": "Sequence Gap Finder",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Audit",
   "description": "Finds missing, duplicate and out-of-order numbers in invoice, cheque or ticket sequences, per series, with a completeness summary.",
   "tags": [
    "sequence gaps",
    "completeness",
    "audit",
    "invoices",
    "duplicates",
    "controls",
    "fraud",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "number_field",
     "title": "Document number field",
     "type": "Field",
     "description": "The document number, with or without a text prefix."
    },
    {
     "name": "series_field",
     "title": "Series field (optional)",
     "type": "Field",
     "description": "Default: the text before the digits."
    },
    {
     "name": "date_field",
     "title": "Document date (optional)",
     "type": "Field",
     "description": "For the date order check."
    }
   ],
   "options": [
    {
     "name": "max_missing_listed",
     "title": "Missing numbers listed per gap",
     "type": "Whole number",
     "description": "A longer gap lists this many numbers and then '...'."
    }
   ],
   "outputs": [
    {
     "id": "gaps",
     "label": "Gaps",
     "description": "Per gap: series, last number before, first after, missing count, missing numbers."
    },
    {
     "id": "problems",
     "label": "Problems",
     "description": "Per problem document: duplicate number or dated before an earlier number."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "Per series: documents, first and last number, expected, missing, gaps, duplicates, out of order, completeness."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks, checking a numbered sequence takes a sort, a previous-value formula and filters per series. This block reports missing ranges, duplicates and out-of-order numbers directly.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Benford-Test",
     "name": "Benford's Law Test",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Benford-Test/index.html",
     "note": "Another audit screen: whether amounts follow Benford's law."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Sequence-Gap-Finder/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Sequence-Gap-Finder",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sequence-Gap-Finder/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Sequence-Gap-Finder/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sequence-Gap-Finder/sample-data/sales-documents.csv"
    ]
   }
  },
  {
   "id": "Preparation/Sessionise-Events",
   "name": "Sessionise Events",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Groups each user's timestamped events into sessions by an inactivity gap, adding session IDs to events and producing a session summary with duration, landing and exit events.",
   "tags": [
    "sessions",
    "clickstream",
    "web analytics",
    "events",
    "logs",
    "sessionisation",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "user_field",
     "title": "User or device field",
     "type": "Field",
     "description": "Who the events belong to: visitor ID, device, card number, badge."
    },
    {
     "name": "time_field",
     "title": "Timestamp field",
     "type": "Field",
     "description": "Date and time of each event."
    },
    {
     "name": "event_field",
     "title": "Event name field (optional)",
     "type": "Field",
     "description": "Page, screen or event type; gives the first and last event of each session."
    },
    {
     "name": "value_field",
     "title": "Value field (optional)",
     "type": "Field",
     "description": "Summed per session, such as order value."
    }
   ],
   "options": [
    {
     "name": "gap_minutes",
     "title": "Inactivity gap (minutes)",
     "type": "Number",
     "description": "A gap longer than this starts a new session. 30 is the web analytics convention."
    },
    {
     "name": "max_session_hours",
     "title": "Maximum session length (hours)",
     "type": "Number",
     "description": "Optional: split sessions longer than this. 0 = no limit."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April."
    }
   ],
   "outputs": [
    {
     "id": "events",
     "label": "Events",
     "description": "Input events with Session ID, Session Number, Event Number In Session, Seconds Since Previous Event, Is Session Start."
    },
    {
     "id": "sessions",
     "label": "Sessions",
     "description": "One row per session: user, start, end, duration, events, first and last event, total value, bounce."
    }
   ],
   "libraries": [],
   "relatedSummary": "With formulas, sessionising takes a sort, a lag of the timestamp per user, a gap test and a running count, then an aggregate. This block does it in one step with session summaries.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Path-Analysis",
     "name": "Path Analysis",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Path-Analysis/index.html",
     "note": "The most common sequences of events per session, using this block's Session ID."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Sessionise-Events/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Sessionise-Events",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sessionise-Events/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Sessionise-Events/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sessionise-Events/sample-data/web-clickstream.csv"
    ]
   }
  },
  {
   "id": "Preparation/Set Project Parameters",
   "name": "Set Project Parameters",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Workflow",
   "description": "Sets project parameters, often of the project it is in, from records holding a parameter name and a value; all values are set in one request.",
   "tags": [
    "parameter",
    "project parameters",
    "param",
    "rest api",
    "orchestration"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "Params_Name",
     "title": "Name field",
     "type": "Field",
     "description": "The field holding the names of the parameters to set."
    },
    {
     "name": "Params_Value",
     "title": "Value field",
     "type": "Field",
     "description": "The field holding the values to give them."
    }
   ],
   "options": [
    {
     "name": "Iox_File_Url",
     "title": "Project address",
     "type": "Text",
     "description": "The address of the project whose parameters to set, ending in .iox. On the same server, the internal address such as http://127.0.0.1:24679/Folder/Project.iox/ avoids the public address and its certificate. A report or workflow address of the project is cut back to the project."
    },
    {
     "name": "username",
     "title": "Username",
     "type": "Text",
     "description": "Optional. For a server that asks for sign-in with a list of users: sent with basic authentication, so use an https:// address unless it is the server's own 127.0.0.1 address. Leave empty when no sign-in is needed."
    },
    {
     "name": "password",
     "title": "Password",
     "type": "Password",
     "description": "Optional. The password for the username."
    }
   ],
   "outputs": [
    {
     "id": "set",
     "label": "Set",
     "description": "The records whose values were set: one per parameter (for a name given twice, the later record)."
    }
   ],
   "libraries": [],
   "relatedSummary": "Project parameters are normally set in the Parameters dialog, by report views that switch a parameter, or through the server's Workflow REST API. This block lets a workflow set several parameters of one project from its own data, one record per parameter, in a single request.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/ForEach/ProjectParameters",
     "name": "Project Parameters Batch Setting",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/ForEach/ProjectParameters/index.html",
     "note": "Sets the same parameters in many projects, one record per project and one field per parameter. Use this block for several parameters of one project, often its own."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "For each",
     "page": null,
     "note": "Changes parameters only for the duration of its loop, in its own project."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Conditional Execution",
     "name": "Conditional Execution",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Conditional%20Execution/index.html",
     "note": "Starts listed blocks when a yes/no parameter, such as one this block sets, is on."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "switchparam",
     "name": "Switch Param",
     "page": "https://visokio.github.io/omniscope-custom-views/switchparam/index.html",
     "note": "Lets a report viewer set one parameter by picking a value."
    },
    {
     "relation": "seeAlso",
     "kind": "view",
     "id": "setparamandexecute",
     "name": "Set Parameters and Execute",
     "page": "https://visokio.github.io/omniscope-custom-views/setparamandexecute/index.html",
     "note": "Lets a report viewer type parameter values and run blocks with them."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Set%20Project%20Parameters/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Set%20Project%20Parameters",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Set%20Project%20Parameters/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Set%20Project%20Parameters/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Set%20Project%20Parameters/sample-data/parameter-values.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Set%20Project%20Parameters/sample-data/placeholder.csv"
    ]
   }
  },
  {
   "id": "Preparation/Smart-Date-Parser",
   "name": "Smart Date Parser",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Parses text fields holding dates in mixed formats (ISO, day-first or month-first, named months, compact, with time zones) into Date fields, and flags the values it could not read.",
   "tags": [
    "date",
    "parsing",
    "dates",
    "mixed formats",
    "time zone",
    "cleansing"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fields_to_parse",
     "title": "Fields to parse",
     "type": "Fields",
     "description": "Text fields holding dates. Fields that are already Date fields are copied unchanged."
    }
   ],
   "options": [
    {
     "name": "output_suffix",
     "title": "Output suffix",
     "type": "Text",
     "description": "Added to each field name for the parsed field (Order Date becomes Order Date_parsed). Leave empty to replace the field in place."
    },
    {
     "name": "prefer_day_first",
     "title": "Day first",
     "type": "Yes / no",
     "description": "Read ambiguous dates such as 04/03/2025 as day/month (4 March). Unticked, they are read month/day (3 April). Dates starting with a four-digit year are always year-month-day."
    },
    {
     "name": "add_failure_flag",
     "title": "Add failure flag",
     "type": "Yes / no",
     "description": "Add a field ending in _failed that is true where a value was present but could not be read."
    },
    {
     "name": "keep_original_fields",
     "title": "Keep original fields",
     "type": "Yes / no",
     "description": "Keep the original text fields next to the parsed ones. Only applies when there is an output suffix."
    },
    {
     "name": "custom_formats",
     "title": "Custom formats",
     "type": "Text",
     "description": "Optional. Python strptime codes, comma-separated, tried before the built-in formats; for example %y%m%d,%d.%m.%Y"
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Parsed",
     "description": "The input with a Date field (and a _failed flag) for each parsed field."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Field organiser** converts a text field to a Date with one format for the whole field, so a field that mixes formats needs splitting, several conversions and an append. This block reads each value on its own, in any of the common formats plus your own, and marks the values it could not read.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Smart-Schema-Normaliser",
     "name": "Smart Schema Normaliser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Schema-Normaliser/index.html",
     "note": "Guesses number, date and yes/no types for every text field of an untidy export."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Date-Parser/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Smart-Date-Parser",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Smart-Date-Parser/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Smart-Date-Parser/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Smart-Date-Parser/sample-data/supplier-deliveries.csv"
    ]
   }
  },
  {
   "id": "Preparation/Smart-Schema-Normaliser",
   "name": "Smart Schema Normaliser",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Cleans up an export with untidy field names: makes the names lower case with underscores, merges fields whose names differ only in case, spaces or dashes, and gives each text field a number, date, yes/no or text type, working through the data in chunks so large inputs never have to fit in memory.",
   "tags": [
    "field",
    "streaming",
    "rename",
    "name",
    "schema",
    "types",
    "normalise",
    "merge fields"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "prefer_non_empty_strings",
     "title": "Prefer non-empty values",
     "type": "Yes / no",
     "description": "When merging fields, an empty text value is replaced by the next field's value, like a missing one."
    },
    {
     "name": "sort_columns",
     "title": "Sort fields by name",
     "type": "Yes / no",
     "description": "Order the output fields alphabetically. Unticked, they keep the order of their first appearance."
    },
    {
     "name": "auto_cast_threshold",
     "title": "Auto cast threshold",
     "type": "Number",
     "description": "Share of a field's values (0 to 1, missing values not counted) that must fit a type for the field to get it. Default 0.98."
    },
    {
     "name": "day_first",
     "title": "Day first",
     "type": "Yes / no",
     "description": "Read ambiguous dates such as 04/03/2024 as 4 March. Unticked, 3 April. Dates starting with a four-digit year are always year-month-day."
    },
    {
     "name": "number_default",
     "title": "Default for missing numbers",
     "type": "Text",
     "description": "Optional. A number (such as 0) to put in empty values of number fields. Empty: they stay missing."
    },
    {
     "name": "datetime_default",
     "title": "Default for missing dates",
     "type": "Text",
     "description": "Optional. A date (such as 1970-01-01) to put in empty values of date fields. Empty: they stay missing."
    },
    {
     "name": "string_default",
     "title": "Default for missing text",
     "type": "Text",
     "description": "Optional. Text to put in empty values of text fields. Empty: they stay missing."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Normalised",
     "description": "The input with tidied field names, merged duplicate fields and inferred types."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Format field names** block changes how names are written, and **Define schema** sets types from a schema you maintain; neither merges two fields that hold the same thing under different names. This block does all three in one step with no field list, so it copes when a new export adds or renames a field.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "Format field names",
     "page": null,
     "note": "Changes how chosen field names are written, such as snake case. This block also merges fields whose names differ only in case or spacing and guesses each text field's type."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Define schema",
     "page": null,
     "note": "Sets names and types from a schema you know, rather than guessing."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Canonical-Schema-Mapper",
     "name": "Canonical Schema Mapper",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Canonical-Schema-Mapper/index.html",
     "note": "Maps aliases to names you choose, from rules kept as data."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Data-Cleansing",
     "name": "Data Cleansing",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Data-Cleansing/index.html",
     "note": "Cleans values instead of names: blanks, placeholders, characters, case."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Smart-Date-Parser",
     "name": "Smart Date Parser",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Date-Parser/index.html",
     "note": "Reads dates written in many formats in one field, and flags those it can't read."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Smart-Schema-Normaliser/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Smart-Schema-Normaliser",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Smart-Schema-Normaliser/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Smart-Schema-Normaliser/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Smart-Schema-Normaliser/sample-data/customer-exports.csv"
    ]
   }
  },
  {
   "id": "Preparation/Sort-fields",
   "name": "Sort fields",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Reorders the fields of the data by name (case-insensitive, with numbers in names in numeric order: Q2 before Q10) or by type, optionally keeping chosen fields first.",
   "tags": [
    "sort",
    "fields",
    "sort fields",
    "reorder",
    "field order",
    "columns"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "firstFields",
     "title": "Fields to keep first",
     "type": "Fields",
     "description": "Optional. These fields stay at the start, in the order chosen; the rest are sorted after them."
    }
   ],
   "options": [
    {
     "name": "sortBy",
     "title": "Sort fields by",
     "type": "Choice",
     "description": "Name: case-insensitive, numbers in names in numeric order. Type: text, numbers, dates, yes/no, then by name."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Sorted",
     "description": "The input with its fields in the new order."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Field organiser** puts fields in the order you arrange them, so a field added to the source later lands wherever the source puts it. This block applies a rule instead (by name, or by type then name), so new fields fall into place by themselves. The core **Sort** block orders records, not fields.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Sort-fields/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Sort-fields",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sort-fields/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Sort-fields/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Sort-fields/sample-data/survey-responses.csv"
    ]
   }
  },
  {
   "id": "Preparation/Split-Address",
   "name": "Split Address",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Splits a street address such as \"Kerkstraat 12A\" or \"221B Baker Street\" into the street name, the house number and a suffix (the letter or addition after the number).",
   "tags": [
    "split",
    "address",
    "street",
    "house number",
    "parse"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "address",
     "title": "Address field",
     "type": "Field",
     "description": "The text field holding the street address (street and number only)."
    }
   ],
   "options": [
    {
     "name": "streetname",
     "title": "Street name field",
     "type": "Text",
     "description": "Name of the new field for the street name. Default: Streetname."
    },
    {
     "name": "streetnumber",
     "title": "House number field",
     "type": "Text",
     "description": "Name of the new field for the house number (digits only, as text). Default: Streetnumber."
    },
    {
     "name": "suffix",
     "title": "Suffix field",
     "type": "Text",
     "description": "Name of the new field for what follows the number (A, bis, Apt B, 2. tv). Default: Suffix."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Split",
     "description": "The input records with three new fields: street name, house number and suffix. Empty when the address was not recognised."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Split values** block splits at a separator character, so it cannot tell whether the number comes before or after the street, or that \"12A\" is a number and a suffix. This block tries a set of patterns for both orders, with suffixes, and expects the postcode to have been split off already.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Geo/UK-Postcode-Tools",
     "name": "UK Postcode Tools",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Geo/UK-Postcode-Tools/index.html",
     "note": "Validates and splits UK postcodes, often the other half of an address clean-up."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Split-Address/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Split-Address",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Split-Address/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Split-Address/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Split-Address/sample-data/delivery-addresses.csv"
    ]
   }
  },
  {
   "id": "Preparation/Standardisation/Standardise",
   "name": "Standardise",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Standardisation",
   "description": "Rescales the chosen number fields to the range 0 to 1 (min-max scaling): each field's lowest value becomes 0, its highest 1, and the rest fall proportionally between.",
   "tags": [
    "standardise",
    "normalise",
    "rescale",
    "min-max",
    "range",
    "interval",
    "scoring"
   ],
   "aiBuilt": false,
   "language": "R",
   "inputs": [
    {
     "name": "fields",
     "title": "Fields to standardise",
     "type": "Fields",
     "description": "The number fields to rescale. Each is rescaled on its own lowest and highest value and replaced in the output."
    }
   ],
   "options": [],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Standardised",
     "description": "The input records with each chosen field rescaled to 0 to 1."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Normalise** block converts number fields to z-scores, which have no fixed range; this block does min-max scaling, so every chosen field runs from exactly 0 to 1. Use it when rescaling is the whole job and R is preferred.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Normalise",
     "page": null,
     "note": "Scales to mean 0 and standard deviation 1 instead of 0 to 1."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Multi-Field-Formula",
     "name": "Multi-Field Formula",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Multi-Field-Formula/index.html",
     "note": "Min-max scaling among many other operations on many fields at once."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Weighted-Scoring",
     "name": "Weighted Scoring",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Weighted-Scoring/index.html",
     "note": "Builds a weighted scorecard, rescaling each criterion itself."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Standardisation/Standardise/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Standardisation/Standardise",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Standardisation/Standardise/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Standardisation/Standardise/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Standardisation/Standardise/sample-data/supplier-scores.csv"
    ]
   }
  },
  {
   "id": "Preparation/Stratified-Sample",
   "name": "Stratified Sample",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Sampling",
   "description": "Draws a reproducible random sample within strata: proportional with a minimum per stratum, equal, fixed number or percentage, with sampling weights and an allocation table.",
   "tags": [
    "sample",
    "stratified",
    "audit sampling",
    "random",
    "split",
    "survey",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "strata_fields",
     "title": "Strata fields",
     "type": "Fields",
     "description": "Sample within each combination of these fields. Leave empty for a simple random sample."
    }
   ],
   "options": [
    {
     "name": "method",
     "title": "Allocation",
     "type": "Choice",
     "description": "How the sample is split between strata."
    },
    {
     "name": "total_n",
     "title": "Total sample size",
     "type": "Whole number",
     "description": "For proportional and equal allocation."
    },
    {
     "name": "min_per_stratum",
     "title": "Minimum per stratum",
     "type": "Whole number",
     "description": "Proportional allocation only: each stratum gets at least this many (or all its records, if fewer)."
    },
    {
     "name": "per_stratum_n",
     "title": "Records per stratum",
     "type": "Whole number",
     "description": "For the fixed-number method."
    },
    {
     "name": "percent",
     "title": "Percentage per stratum",
     "type": "Number",
     "description": "For the percentage method, 0 to 100."
    },
    {
     "name": "seed",
     "title": "Random seed",
     "type": "Whole number",
     "description": "The same seed always gives the same sample."
    }
   ],
   "outputs": [
    {
     "id": "sample",
     "label": "Sample",
     "description": "Sampled records with Sample Weight and Stratum."
    },
    {
     "id": "remainder",
     "label": "Not sampled",
     "description": "All records not in the sample."
    },
    {
     "id": "allocation",
     "label": "Allocation",
     "description": "Per stratum: population, sample size, sampling fraction, weight, shares."
    }
   ],
   "libraries": [],
   "relatedSummary": "Neither the core **Random Sample** nor the **Split Data** block samples within strata; this block draws a reproducible sample per group: proportional with a minimum, equal, or a fixed number.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Random Sample",
     "page": null,
     "note": "A fixed number or percentage of rows, without strata."
    },
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Split Data",
     "page": null,
     "note": "Proportional or cut-off split into two outputs."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Stratified-Sample/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Stratified-Sample",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Stratified-Sample/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Stratified-Sample/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Stratified-Sample/sample-data/invoice-population.csv"
    ]
   }
  },
  {
   "id": "Preparation/Streaming-Field-Renamer",
   "name": "Streaming Field Renamer",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Renames the fields of the input from rename rules (current name, new name) written in the block or kept in a CSV file, working through the data in chunks so large inputs never have to fit in memory.",
   "tags": [
    "field",
    "streaming",
    "rename",
    "name",
    "mapping",
    "schema"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [],
   "options": [
    {
     "name": "rulesText",
     "title": "Rename rules",
     "type": "Text",
     "description": "Rules written in the block, one per line or separated by semicolons: Current Name = New Name. Takes precedence over the file."
    },
    {
     "name": "rules",
     "title": "Rename rules CSV file",
     "type": "File",
     "description": "A CSV file with the columns Current Name and New Name, one rule per field to rename. UTF-8 text; the first line holds the column headings. Used when Rename rules is empty."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Renamed",
     "description": "The input with its fields renamed by the rules; values unchanged."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Field organiser** and **Bulk field organiser** rename fields in each block's own settings. This block takes the renames as one short list that several projects can share, and streams the data rather than loading it all.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Field-Renamer",
     "name": "Field Renamer",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Field-Renamer/index.html",
     "note": "Reads the rename rules from a second input (a table from any block) and lists unused rules. Use this block for very large inputs or rules kept in one CSV file."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Canonical-Schema-Mapper",
     "name": "Canonical Schema Mapper",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Canonical-Schema-Mapper/index.html",
     "note": "Maps several aliases to one name and converts types, also streaming."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Streaming-Field-Renamer/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Streaming-Field-Renamer",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Streaming-Field-Renamer/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Streaming-Field-Renamer/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Streaming-Field-Renamer/sample-data/bank-transactions.csv",
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Streaming-Field-Renamer/sample-data/rename-rules.csv"
    ]
   }
  },
  {
   "id": "Preparation/Subtotals-Rollup",
   "name": "Subtotals and Rollup",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Aggregate",
   "description": "Aggregates by a hierarchy of fields with subtotal rows at every level and a grand total, shares of parent and total, in report order.",
   "tags": [
    "subtotals",
    "rollup",
    "grouping sets",
    "hierarchy",
    "finance table",
    "management accounts",
    "aggregate",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "group_fields",
     "title": "Group fields, outermost first",
     "type": "Fields",
     "description": "For example Division, Region, Site."
    },
    {
     "name": "value_fields",
     "title": "Value fields",
     "type": "Fields",
     "description": "The numbers to aggregate at every level."
    }
   ],
   "options": [
    {
     "name": "aggregation",
     "title": "Aggregate",
     "type": "Choice",
     "description": "How values are combined into subtotals."
    },
    {
     "name": "sort",
     "title": "Order children by",
     "type": "Choice",
     "description": "Order of rows under each subtotal."
    },
    {
     "name": "subtotal_position",
     "title": "Subtotals",
     "type": "Choice"
    },
    {
     "name": "grand_total",
     "title": "Grand total row",
     "type": "Yes / no"
    },
    {
     "name": "total_label",
     "title": "Total label",
     "type": "Text"
    }
   ],
   "outputs": [
    {
     "id": "rollup",
     "label": "Rollup",
     "description": "Order, group fields, level, row type, indented label, rows, values, share of parent, share of total."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Aggregate** block summarises one grouping at a time, so a table with subtotal rows at every level and a grand total needs one Aggregate per level and an Append. This block builds it in one step, with shares of parent and of total.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Aggregate",
     "page": null,
     "note": "Summary measures per group, one level at a time."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Top-N-With-Other",
     "name": "Top N With Other",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Top-N-With-Other/index.html",
     "note": "Keeps the top items per group and rolls the rest into Other."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Subtotals-Rollup/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Subtotals-Rollup",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Subtotals-Rollup/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Subtotals-Rollup/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Subtotals-Rollup/sample-data/divisional-sales.csv"
    ]
   }
  },
  {
   "id": "Preparation/Tile",
   "name": "Tile",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Binning",
   "description": "Assigns records to tiles of a numeric field: quantiles, equal-sum Pareto bands, equal width, standard-deviation bands or manual cut points, optionally within groups.",
   "tags": [
    "tile",
    "binning",
    "quantile",
    "decile",
    "pareto",
    "bands",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "value_field",
     "title": "Value field",
     "type": "Field",
     "description": "The numeric field to tile."
    },
    {
     "name": "group_fields",
     "title": "Within groups (optional)",
     "type": "Fields",
     "description": "Tile separately within each combination of these fields."
    }
   ],
   "options": [
    {
     "name": "method",
     "title": "Method",
     "type": "Choice",
     "description": "How the tile boundaries are set."
    },
    {
     "name": "tiles",
     "title": "Number of tiles",
     "type": "Whole number",
     "description": "4 for quartiles, 10 for deciles, 100 for percentiles. Not used by smart and manual tiles."
    },
    {
     "name": "cut_points",
     "title": "Manual cut points",
     "type": "Text",
     "description": "Comma-separated upper bounds, such as 1000, 5000, 20000 (values equal to a cut point go to the next tile up)."
    },
    {
     "name": "highest_first",
     "title": "Tile 1 = highest values",
     "type": "Yes / no",
     "description": "Untick for tile 1 = lowest values."
    },
    {
     "name": "field_prefix",
     "title": "Name of new fields",
     "type": "Text",
     "description": "New fields are '<name> Number' and '<name> Range'."
    }
   ],
   "outputs": [
    {
     "id": "tiled",
     "label": "Tiled",
     "description": "The input with Tile Number and Tile Range (or Tile Label)."
    },
    {
     "id": "summary",
     "label": "Summary",
     "description": "One row per (group and) tile: records, minimum, maximum, total, share of total, share of records."
    }
   ],
   "libraries": [],
   "relatedSummary": "The BUCKET formula makes equal-width bands only. This block adds quantiles, equal-sum (Pareto) bands, standard-deviation bands and manual cut points, within groups.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Partition",
     "name": "Partition",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Partition/index.html",
     "note": "Numbers records in chunks of a fixed size, regardless of their values."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/ABC-Classification",
     "name": "ABC Classification",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/ABC-Classification/index.html",
     "note": "A, B and C classes by cumulative share of value, with XYZ demand variability."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Histogram-Distribution",
     "name": "Histogram and Distribution",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Histogram-Distribution/index.html",
     "note": "Counts records per bin for a histogram, with distribution statistics."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Tile/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Tile",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Tile/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Tile/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Tile/sample-data/customer-revenue.csv"
    ]
   }
  },
  {
   "id": "Preparation/Time-Duration-Unit-Converter",
   "name": "Time Duration Unit Converter",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Turns times of day and durations into a number of milliseconds, seconds, minutes or hours, in a new field for each chosen field, ready to add up, average or compare.",
   "tags": [
    "duration",
    "time",
    "time of day",
    "convert",
    "seconds",
    "minutes",
    "hours"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_fields",
     "title": "Fields to convert",
     "type": "Fields",
     "description": "Date fields (time since midnight) or text fields holding durations or times."
    }
   ],
   "options": [
    {
     "name": "unit",
     "title": "Unit",
     "type": "Choice",
     "description": "The unit of the new fields. Its letter (ms, s, m or h) is added to each new field's name."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Converted",
     "description": "The input records with a new number field for each chosen field, named after it with the unit added (_ms, _s, _m or _h)."
    }
   ],
   "libraries": [],
   "relatedSummary": null,
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Time-Duration-Unit-Converter/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Time-Duration-Unit-Converter",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Time-Duration-Unit-Converter/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Time-Duration-Unit-Converter/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Time-Duration-Unit-Converter/sample-data/support-calls.csv"
    ]
   }
  },
  {
   "id": "Preparation/Time-Series-Filler",
   "name": "Time Series Filler",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Inserts missing days, weeks, months or other periods in each time series and fills values with zero, the previous value or interpolation, with a gap report.",
   "tags": [
    "time series",
    "gaps",
    "fill",
    "calendar",
    "zero fill",
    "interpolate",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "date_field",
     "title": "Date field",
     "type": "Field",
     "description": "Date or date-time of each record."
    },
    {
     "name": "group_fields",
     "title": "Series (optional)",
     "type": "Fields",
     "description": "One series per combination of these fields, such as Product or Store and Product."
    },
    {
     "name": "value_fields",
     "title": "Value fields to fill",
     "type": "Fields",
     "description": "Fields to fill in inserted rows."
    }
   ],
   "options": [
    {
     "name": "frequency",
     "title": "One row per",
     "type": "Choice",
     "description": "The period to insert missing rows at."
    },
    {
     "name": "fill_method",
     "title": "Fill with",
     "type": "Choice",
     "description": "What the value fields get in inserted rows."
    },
    {
     "name": "carry_other",
     "title": "Carry other fields forward",
     "type": "Yes / no",
     "description": "Copy the remaining fields (such as Category) from the previous row into inserted rows."
    },
    {
     "name": "span",
     "title": "Each series runs",
     "type": "Choice",
     "description": "Where each series starts and ends: its own data, the whole dataset or a custom range."
    },
    {
     "name": "range_start",
     "title": "Custom range start",
     "type": "Text",
     "description": "For example 2025-01-01."
    },
    {
     "name": "range_end",
     "title": "Custom range end",
     "type": "Text",
     "description": "For example 2025-12-31."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April."
    }
   ],
   "outputs": [
    {
     "id": "filled",
     "label": "Filled",
     "description": "Every series with one row per period; Period label and Is Inserted flag added."
    },
    {
     "id": "gaps",
     "label": "Gaps",
     "description": "Per series: first and last period, expected and present periods, inserted periods, longest gap, completeness."
    }
   ],
   "libraries": [],
   "relatedSummary": "With core blocks, inserting the missing periods of each series takes a calendar, a cross join with the series keys and a join back. This block does it in one step with a choice of fill method.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Imputation",
     "name": "Imputation",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Imputation/index.html",
     "note": "Fills missing values in existing rows by mean, median, mode or interpolation, within groups."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Time-Series-Filler/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Time-Series-Filler",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Time-Series-Filler/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Time-Series-Filler/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Time-Series-Filler/sample-data/daily-product-sales.csv"
    ]
   }
  },
  {
   "id": "Preparation/Top-N-With-Other",
   "name": "Top N With Other",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Aggregate",
   "description": "Keeps the top N items per group by value and rolls the rest into an Other row, with ranks, shares and cumulative shares.",
   "tags": [
    "top N",
    "other",
    "ranking",
    "reporting",
    "pie chart",
    "pareto",
    "aggregate",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "item_field",
     "title": "Item field",
     "type": "Field",
     "description": "For example Product or Supplier."
    },
    {
     "name": "value_field",
     "title": "Rank by value",
     "type": "Field",
     "description": "The number items are ranked on, such as revenue."
    },
    {
     "name": "group_fields",
     "title": "Per group (optional)",
     "type": "Fields",
     "description": "For example Region."
    },
    {
     "name": "extra_value_fields",
     "title": "Other values to carry (optional)",
     "type": "Fields",
     "description": "Other numbers to total into the Other row alongside the ranking value."
    }
   ],
   "options": [
    {
     "name": "top_n",
     "title": "Top N",
     "type": "Whole number",
     "description": "0 = no limit (use the minimum share)."
    },
    {
     "name": "min_share",
     "title": "Minimum share of group",
     "type": "Number",
     "description": "For example 0.02; 0 = none."
    },
    {
     "name": "include_other",
     "title": "Add an Other row",
     "type": "Yes / no",
     "description": "Adds a row totalling the items outside the top N."
    },
    {
     "name": "other_label",
     "title": "Label for Other",
     "type": "Text"
    }
   ],
   "outputs": [
    {
     "id": "top",
     "label": "Top N",
     "description": "Per group: kept items and an Other row, with rank, values, share of group, cumulative share, items included."
    }
   ],
   "libraries": [],
   "relatedSummary": "Top N with an Other row is possible with **Aggregate**, **Sort**, a rank formula and an **Append**, but fiddly per group. This block keeps the top N per group and rolls the rest into an Other row, ready for a readable chart.",
   "related": [
    {
     "relation": "seeAlso",
     "kind": "coreBlock",
     "id": null,
     "name": "Aggregate",
     "page": null,
     "note": "Summary measures per group, without ranking or an Other row."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/Subtotals-Rollup",
     "name": "Subtotals and Rollup",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Subtotals-Rollup/index.html",
     "note": "Subtotals at every level of a hierarchy, with a grand total."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Analytics/Concentration-Measures",
     "name": "Concentration Measures",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Analytics/Concentration-Measures/index.html",
     "note": "Summarises how concentrated a total is: HHI, Gini, top-N shares."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Top-N-With-Other/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Top-N-With-Other",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Top-N-With-Other/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Top-N-With-Other/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Top-N-With-Other/sample-data/regional-sales.csv"
    ]
   }
  },
  {
   "id": "Preparation/URL-Encode",
   "name": "URL Encode",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Percent-encodes the text in a field (UTF-8) into a new field, so it can go into a link, a query string or a web request.",
   "tags": [
    "url",
    "quote",
    "encode",
    "percent-encoding",
    "link",
    "query string"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fieldName",
     "title": "Text to encode",
     "type": "Field",
     "description": "The text field to encode."
    }
   ],
   "options": [
    {
     "name": "outputField",
     "title": "Encoded field name",
     "type": "Text",
     "description": "Name of the new field holding the encoded text. Use the name of the field being encoded to replace it."
    },
    {
     "name": "encodeSlashes",
     "title": "Encode slashes",
     "type": "Yes / no",
     "description": "Encode / as %2F too. Unticked, / is kept."
    },
    {
     "name": "spacesAsPlus",
     "title": "Spaces as +",
     "type": "Yes / no",
     "description": "Encode spaces as + (form style) instead of %20."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Encoded",
     "description": "The input records with the encoded text in a new field."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Search / replace** block changes text by pattern, so encoding by hand takes one replacement per character, and accented letters need their UTF-8 byte codes worked out. This block applies standard percent-encoding to the whole field in one step, with a choice of keeping slashes and of + for spaces.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/URL-Encode/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/URL-Encode",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/URL-Encode/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/URL-Encode/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/URL-Encode/sample-data/search-terms.csv"
    ]
   }
  },
  {
   "id": "Preparation/Unescape-HTML",
   "name": "Unescape HTML",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Turns HTML character references such as &amp;, &eacute; and &#163; in a text field back into the characters they stand for, in a new field.",
   "tags": [
    "html",
    "escape",
    "unescape",
    "entities",
    "decode",
    "text cleaning"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "fieldName",
     "title": "Text to unescape",
     "type": "Field",
     "description": "The text field holding the escaped text."
    }
   ],
   "options": [
    {
     "name": "outputField",
     "title": "Unescaped field name",
     "type": "Text",
     "description": "Name of the new field holding the unescaped text. Use the name of the field being unescaped to replace it."
    },
    {
     "name": "repeat",
     "title": "Repeat for double-escaped text",
     "type": "Yes / no",
     "description": "Unescape again until nothing changes, so &amp;lt; becomes < rather than &lt;."
    },
    {
     "name": "plainSpaces",
     "title": "Non-breaking spaces as plain spaces",
     "type": "Yes / no",
     "description": "Turn non-breaking spaces (&nbsp;) into ordinary spaces."
    }
   ],
   "outputs": [
    {
     "id": "Output Data",
     "label": "Unescaped",
     "description": "The input records with the unescaped text in a new field."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **Search / replace** block can swap `&amp;` for `&`, one reference at a time, but HTML has over two thousand named references plus numeric ones, and `&amp;` must be done last or it creates new ones. This block converts them all in one pass, with the same rules web browsers use.",
   "related": [],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Unescape-HTML/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Unescape-HTML",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Unescape-HTML/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Unescape-HTML/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Unescape-HTML/sample-data/product-feed.csv"
    ]
   }
  },
  {
   "id": "Preparation/Unstack-rows",
   "name": "Unstack Records",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": null,
   "description": "Splits text fields that hold several values in one cell, such as one item per line, into one record per value, repeating the other fields.",
   "tags": [
    "unstack",
    "split",
    "explode",
    "multi-value",
    "reshape"
   ],
   "aiBuilt": false,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "textFields",
     "title": "Fields to unstack",
     "type": "Fields",
     "description": "Text fields holding several values each. Values in the chosen fields are paired by position."
    }
   ],
   "options": [
    {
     "name": "separator",
     "title": "Separator",
     "type": "Text",
     "description": "The text between values. \\n is a line break (the default), \\t a tab, \\r a carriage return; anything else is matched exactly."
    }
   ],
   "outputs": [
    {
     "id": "Output",
     "label": "Unstacked",
     "description": "One record per value in the chosen fields, with the other fields repeated."
    }
   ],
   "libraries": [],
   "relatedSummary": "The core **De-tokenise** block handles one field; this block also fills shorter lists with empty values and accepts a separator of any length.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "coreBlock",
     "id": null,
     "name": "De-tokenise",
     "page": null,
     "note": "Splits tokenised text in one field into separate records. This block splits several fields at once and pairs their values by position (each product with its quantity)."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Preparation/RegEx-Parse",
     "name": "RegEx Parse",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/RegEx-Parse/index.html",
     "note": "Tokenises the matches of a pattern into rows, when the values have no fixed separator."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Unstack-rows/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Unstack-rows",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Unstack-rows/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Unstack-rows/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Unstack-rows/sample-data/stacked-orders.csv"
    ]
   }
  },
  {
   "id": "Preparation/Working-Days",
   "name": "Working Days",
   "category": "Preparation",
   "categories": [
    "Preparation"
   ],
   "subcategory": "Date",
   "description": "Counts working days between dates and adds working days to dates, skipping weekends and UK or US public holidays, with SLA checks.",
   "tags": [
    "working days",
    "business days",
    "bank holidays",
    "sla",
    "due date",
    "networkdays",
    "calendar",
    "visokio-agents"
   ],
   "aiBuilt": true,
   "language": "PYTHON",
   "inputs": [
    {
     "name": "start_field",
     "title": "Start date",
     "type": "Field",
     "description": "For example the date a ticket was raised or an order placed."
    },
    {
     "name": "end_field",
     "title": "End date (to count working days)",
     "type": "Field",
     "description": "For example the date it was resolved or delivered."
    },
    {
     "name": "days_field",
     "title": "Working days to add (field)",
     "type": "Field",
     "description": "A number of working days per row, such as the SLA for its priority."
    }
   ],
   "options": [
    {
     "name": "days_constant",
     "title": "Working days to add (fixed)",
     "type": "Whole number",
     "description": "Used when no field is chosen. 0 = do not add."
    },
    {
     "name": "include_start",
     "title": "Count the start day",
     "type": "Yes / no",
     "description": "Ticked: Monday to Friday of the same week is 5 working days. Unticked: 4."
    },
    {
     "name": "region",
     "title": "Public holidays",
     "type": "Choice",
     "description": "Public holidays that are not working days."
    },
    {
     "name": "custom_holidays",
     "title": "Extra closure dates",
     "type": "Text",
     "description": "Comma-separated dates, such as 2025-12-29, 2025-12-30, 2025-12-31."
    },
    {
     "name": "weekend",
     "title": "Weekend",
     "type": "Choice",
     "description": "Days that are not working days."
    },
    {
     "name": "day_first",
     "title": "Text dates are day first",
     "type": "Yes / no",
     "description": "Read 03/04/2025 as 3 April."
    }
   ],
   "outputs": [
    {
     "id": "result",
     "label": "Working days",
     "description": "The input with Start Is Working Day, Working Days, Calendar Days, Due Date, Met Target and Working Days Late as applicable."
    },
    {
     "id": "holidays",
     "label": "Holidays",
     "description": "Every holiday used: date, name, region, weekday."
    }
   ],
   "libraries": [],
   "relatedSummary": "The NETWORKDAYS formula ignores public holidays and cannot add working days to a date. This block does both, with generated UK or US holidays and your own closures.",
   "related": [
    {
     "relation": "overlaps",
     "kind": "block",
     "id": "Preparation/Business-Hours-SLA",
     "name": "Business Hours and SLA",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Business-Hours-SLA/index.html",
     "note": "Measures elapsed hours within opening hours and checks SLAs set in business hours. Use this block for SLAs and due dates in whole days."
    },
    {
     "relation": "seeAlso",
     "kind": "block",
     "id": "Inputs/Date-Dimension",
     "name": "Date Dimension",
     "page": "https://visokio.github.io/omniscope-custom-blocks/Inputs/Date-Dimension/index.html",
     "note": "A full calendar table with working-day flags and counts, to join to your data."
    }
   ],
   "links": {
    "page": "https://visokio.github.io/omniscope-custom-blocks/Preparation/Working-Days/index.html",
    "source": "https://github.com/visokio/omniscope-custom-blocks/tree/going-public/Preparation/Working-Days",
    "demoIoz": "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Working-Days/demo.ioz",
    "demoServer": "https://public.omniscope.me/Community/Blocks/Preparation/Working-Days/demo.iox/",
    "sampleData": [
     "https://github.com/visokio/omniscope-custom-blocks/raw/going-public/Preparation/Working-Days/sample-data/support-tickets.csv"
    ]
   }
  }
 ]
}
