# Logic Scripts

Logic Scripts are reusable routing scripts for funnel logic nodes. A script reads visitor, request, tracking-field, visitor-tag, or submitted-form context and returns a route key. Funnel connections from a logic node then match that key through `connectionLogicParams.onRouteKeys[]`.

Logic Scripts are global assets. Create and manage them through the Assets API, then reference the Logic Script asset from a funnel logic node.

## Runtime model

A Logic Script is a Go-statement snippet, not a full Go file. Do not include `package`, `import`, or a function wrapper. Every path must return a string route key.

```go
if country == "US" {
	return "us"
}

if oneOf(country, "CA", "AU", "NZ") {
	return "tier_one"
}

return "default"
```

Route keys must match `^[a-z0-9_]{1,64}$`. The reserved fallback key is `default`.

Always connect a `default` route from each funnel logic node. Runtime uses that connection when the script cannot safely choose one of your other connected routes. This covers invalid code, execution errors, missing data, or code that returns a route key you did not connect.

Keep route keys explicit where possible. For example, `return "us"` is easy for the API to validate and easy for a funnel connection to match. Returning a variable can work at runtime, but the API cannot know every value that variable may contain, so you must be extra careful to connect `default`.

## Funnel wiring

For each connection leaving a logic node, set `connectionLogicParams.onRouteKeys[]` to the route keys that should use that connection.

```json
{
  "connectionLogicParams": {
    "onRouteKeys": ["us", "tier_one"]
  }
}
```

Use one connection with `onRouteKeys: ["default"]` as the runtime fallback. This should go somewhere safe, such as a general offer path, a catch-all lander, or an internal diagnostic path.

## API workflow

The usual flow is:

1. Create or update a Logic Script asset through the Assets API.
2. Validate the script before saving or before wiring it into a funnel.
3. Create a funnel logic node that references the Logic Script asset.
4. Connect each returned route key through `connectionLogicParams.onRouteKeys[]`.
5. Connect `default` as the fallback route.

See the [Logic Scripts API](/api/assets/logic-scripts) for the full Logic Script asset endpoints.

Builder and editor integrations should also use the helper endpoints in the Assets API:

```text
GET  /v1/logicscripts/language
Returns the backend-owned language catalog: variables, helpers, operators, route-key rules, max code size, and templates.

POST /v1/logicscripts/validate
Validates { "code": "..." } and returns whether the script is valid, which route keys were found, and any errors.
```

## Language rules

Maximum code size is 10,240 bytes.

### String variables

These string values are available inside every Logic Script:

```go
country
city
region
continent
timezone
deviceType
brand
model
os
osVersion
browser
browserVersion
mainLanguage
browserLanguage
ip
isp
connectionType
mobileCarrier
userAgent
referrer
initialReferrer
currentURL
trackingDomain
trafficSourceID
```

### Integer variables

These integer values are available inside every Logic Script:

```go
hour
minute
weekday
dayOfMonth
month
year
utcOffsetHours
```

### Useful notes

- Time variables are UTC.
- `weekday` uses Go values, where Sunday is `0`.
- `month` uses Go values, where January is `1`.
- `continent` is a full name such as `Asia`, `Europe`, `North America`, or `Oceania`, not a continent code.
- Logic Script variables mirror public URL token formatting where possible.
- `timezone` is formatted like the public `{timezone}` token, such as `GMT+7` or `GMT-5`.
- `utcOffsetHours` is the matching whole-hour integer offset for time helpers, such as `7`, `-5`, or `0`.
- In general, string variables will have the same values that you see in FunnelFlux reporting.

### Helpers

These helper functions can be used inside your scripts:

```go
len(value string) int

trackingField(name string) string
dataBuffer(name string) string
hasVisitorTag(id string) bool

oneOf(value string, candidates ...string) bool
containsAny(value string, substrings ...string) bool

timeInRange(startHHMM int, endHHMM int, utcOffsetHours int) bool
timeAtOrAfter(hhmm int, utcOffsetHours int) bool
timeAtOrBefore(hhmm int, utcOffsetHours int) bool
localDay(utcOffsetHours int) string

strings.Contains(value string, substr string) bool
strings.HasPrefix(value string, prefix string) bool
strings.HasSuffix(value string, suffix string) bool
strings.ToLower(value string) string
strings.ToUpper(value string) string
```

`trackingField()` and `dataBuffer()` names must match `[A-Za-z0-9_ -]{1,64}`. Use `trackingField("utm_source")` instead of map syntax such as `trackingFields["utm_source"]`.

Time helpers use explicit integer UTC hour offsets, not display strings. Pass `utcOffsetHours` or a literal like `-7`, `0`, or `3`. Do not pass the `timezone` string variable to `timeInRange()`, `timeAtOrAfter()`, `timeAtOrBefore()`, or `localDay()`.

`timeInRange(startHHMM, endHHMM, utcOffsetHours)` uses HHMM integer times and supports overnight wraparound. For example, `timeInRange(2200, 600, 3)` means 22:00 through 06:00 in UTC+3.

### Allowed operators

Use these operators for comparisons and boolean logic:

```text
==
!=
<
>
<=
>=
&&
||
!
```

### Allowed syntax

Use these Go statement forms for readable deterministic routing:

- `if` / `else`
- `switch`
- Local variables
- Local non-recursive functions or closures with typed bool, int, or string arguments and exactly one bool, int, or string return value
- Closures must return on every path
- Comments with `//` or `/* */`
- Local `[]string` literals for helper inputs

### Do not use

These language features and built-ins are blocked:

- Imports
- Package declarations
- Loops
- Goroutines
- Maps or indexing
- Pointers
- Unsupported packages
- Arbitrary method calls
- The following Go functions and statements: `defer`, `select`, `append`, `make`, `new`, `panic`, `print`, `println`, `recover`, `delete`, `copy`

However, list expansion is specifically supported for helper calls, for example:

```go
blocked := []string{"bad isp llc", "example proxy network"}

if oneOf(strings.ToLower(isp), blocked...) {
	return "blocked"
}

return "default"
```

## Examples

Country tier routing:

```go
if country == "US" {
	return "us"
}

if oneOf(country, "CA", "AU", "NZ") {
	return "tier_one"
}

if oneOf(country, "MX", "ES", "AR", "CL", "CO", "PE") {
	return "spanish_speakers"
}

return "default"
```

Business-hours routing:

```go
isWorkDay := oneOf(localDay(utcOffsetHours),
	"monday", "tuesday", "wednesday", "thursday", "friday")

if isWorkDay && timeInRange(700, 1800, utcOffsetHours) {
	return "business_hours"
}

return "default"
```

Overnight local-time routing:

```go
// Uses the visitor's UTC offset. Do not pass the timezone string.
if timeInRange(2200, 600, utcOffsetHours) {
	return "overnight"
}

return "default"
```

Local closure routing:

```go
isMobile := func() bool {
	return deviceType == "mobile"
}

isBusinessHour := func(offset int) bool {
	return timeInRange(900, 1700, offset)
}

if isMobile() && isBusinessHour(utcOffsetHours) {
	return "mobile_business_hours"
}

return "default"
```

Tracking-field routing:

```go
source := strings.ToLower(trackingField("utm_source"))

switch {
case source == "":
	return "is_blank"
case source == "google_ads":
	return "google_ads"
case strings.Contains(source, "newsletter"):
	return "newsletter"
}

return "default"
```

Data-buffer form routing:

```go
answer := strings.ToLower(dataBuffer("question_1"))
consent := strings.ToLower(dataBuffer("consent"))

if oneOf(answer, "a", "b") {
	return "answer_ab"
}

if oneOf(consent, "true", "yes", "1") {
	return "consented"
}

return "default"
```

---

## AI prompt

Use this section in two parts. Copy the short prompt first, edit the goal and route keys, then paste the documentation block afterward or at the end of the same message.

Some AI tools handle long pastes differently. Claude may turn the documentation block into a text snippet or attachment, while other tools such as ChatGPT may paste it directly into the chat. Pasting the editable prompt first keeps the part you need to change easy to deal with.

### Instruction Prompt

```markdown
You are writing a FunnelFlux Pro Logic Script.

# Goal

<describe the routing goal in plain language — the conditions you
want to match and roughly where each should go. You don't need to
name the route keys; choose clear, semantic ones yourself.>

# Reference

Use the attached document titled:
"Logic Script behavior and syntax documentation"

It has the full code format, allowed variables, helper functions,
operators, syntax rules, and restrictions.

# Output

Return three things:

1. A one-sentence, plain-text description of what the script does,
   concise enough to paste into the script's description field in
   FunnelFlux (no formatting, no route-key jargon dump).
2. The Logic Script code in a code snippet box (or artifact), with a
   short comment above each route explaining when it fires.
3. A bullet list of the route keys you used, each with a one-line
   note on when it triggers, so I know what to wire up in the funnel.

Begin the code with a brief comment naming the script's purpose.
```


### Logic Script behavior and syntax documentation

<div className="ff-scroll-code">

```markdown
# Logic Script behavior and syntax documentation

FunnelFlux Logic Scripts are reusable routing scripts for funnel logic nodes.

A script reads visitor, request, tracking-field, visitor-tag, time, and
submitted-form context, then returns a single route key as a string.

Funnel connections leaving the logic node match the returned route key
through `connectionLogicParams.onRouteKeys[]`.

## Core format

- A Logic Script is a Go-statement snippet, not a full Go file.
- Do not include package declarations.
- Do not include imports.
- Do not include a function wrapper.
- Every execution path must return a string route key.
- Statements run top to bottom; the first `return` reached wins, so order
  your rules from most specific to most general.
- Maximum code size is `10240` bytes.

## Route-key rules

- Route keys must match `^[a-z0-9_]{1,64}$`.
- Use lowercase letters, numbers, and underscores.
- Use semantic keys such as `business_hours` or `blocked_isp`, not `a`/`b`.
- Prefer explicit string returns such as `return "us"`.
- Avoid returning a variable as a route key unless the funnel has a safe
  default connection to catch unexpected values.
- Always include a `default` return path as the final statement.

## The `default` route: what it is for

`default` is the safety net for things going wrong, not a normal business
outcome. The default *connection* in the funnel is used when:

- the code is invalid or fails to execute,
- required data is missing due to a system error (for example, IP
  geolocation failed, so `country` is empty),
- or the script returns a route key that is not connected to anything.

Because of this, you normally do **not** write explicit branches for
error-state empty values. For example, you would not add an
`if country == "" { ... }` branch — a blank country means IP resolution
failed on our side, which is an error condition, and letting it fall
through to the final `return "default"` is the correct handling.

This is different from a deliberate "matched none of my rules" outcome.
When a value resolves correctly but simply doesn't match any of your
business rules, give that its own named route (for example, `unmatched`)
rather than folding it into `default`. That keeps real traffic separable
from genuine errors in reporting and wiring.

## Funnel wiring rules

These matter when manipulating Logic Scripts inside funnels via API. If you
are only writing isolated script code, you can ignore them.

- Each connection leaving a logic node uses
  `connectionLogicParams.onRouteKeys[]`.
- Connect every route key your script can return to a destination.
- Always connect `onRouteKeys ["default"]` to a safe fallback destination.

> Builder/editor integrations can use the Assets API helper endpoints:
> `GET /v1/logicscripts/language` returns the backend-owned catalog
> (variables, helpers, operators, route-key rules, max size, templates),
> and `POST /v1/logicscripts/validate` checks a script and returns whether
> it is valid, which route keys it found, and any errors.

## Variable context

- Variables mirror how URL `{tokens}` work in the runtime routing system.
- Tokens are replaced by data resolved from the current tracked visitor.
- Geolocation values are derived from the visitor IP.
- All variables concern the specific visitor currently tracked in a funnel.
- `trackingDomain` is the custom domain used at redirect time.
- `trafficSourceID` is an internal FunnelFlux traffic source asset ID.

## Available string variables

`country`, `city`, `region`, `continent`, `timezone`, `deviceType`,
`brand`, `model`, `os`, `osVersion`, `browser`, `browserVersion`,
`mainLanguage`, `browserLanguage`, `ip`, `isp`, `connectionType`,
`mobileCarrier`, `userAgent`, `referrer`, `initialReferrer`,
`currentURL`, `trackingDomain`, `trafficSourceID`

## Available integer variables

`hour`, `minute`, `weekday`, `dayOfMonth`, `month`, `year`, `utcOffsetHours`

## Value notes

- Time variables (`hour`, `minute`, `weekday`, etc.) are UTC and refer to
  the current timestamp. For visitor-local time, use the time helpers with
  `utcOffsetHours` rather than these raw UTC values.
- `weekday` uses Go values: Sunday is `0`, Saturday is `6`.
- `month` uses Go values: January is `1`, December is `12`.
- `country` is an ISO country code such as `US` or `TH` (uppercase).
- `country` is effectively always populated for a real visitor. It is
  empty only when IP geolocation fails — treat that as an error and let it
  reach `default` (see "The `default` route").
- `continent` is a full name such as `Asia`, `Europe`, `North America`, or
  `Oceania` — not a code such as `AS` or `EU`.
- `city`, `region`, `isp`, `connectionType`, `mobileCarrier`, `deviceType`,
  `brand`, `model`, `os`, `browser`, and similar values are resolved from
  the visitor IP and user agent. When a specific value can't be determined
  they typically return the literal string `unknown`, not an empty string.
  An empty value therefore signals a resolution error — which is exactly
  what `default` is for. Do not write branches for any of these being
  blank; an unresolved IP or user agent is an error state, not a routing
  case. (If you ever need to catch a non-resolving value deliberately,
  test for `== "unknown"`, not `== ""`.)
- `referrer` is the current request referrer, falling back to
  `initialReferrer` when empty. An empty `referrer` is a normal, expected
  state (direct traffic, stripped referrer) — branch on it deliberately if
  your logic cares about it. This is the main string variable where blank
  is a real case rather than an error.
- `trafficSourceID` is an internal traffic source asset ID (roughly a
  14-character alphanumeric string). You cannot know a specific ID unless
  the user provides it. The one fixed, system-level value you can rely on
  is `organic`: organically tracked traffic always has
  `trafficSourceID == "organic"` (lowercase).
- `timezone` is a display string formatted like the public `{timezone}`
  token — `GMT+7`, `GMT-5`, `GMT+0`. Do **not** pass it to the time
  helpers; they take an integer offset, not this string.
- `utcOffsetHours` is the matching whole-hour integer offset for the same
  visitor (`7`, `-5`, `0`). This is the value you pass to `timeInRange()`,
  `timeAtOrAfter()`, `timeAtOrBefore()`, and `localDay()` for
  visitor-local time routing. You do not hardcode an offset — use this
  variable so each visitor is evaluated in their own local time.
- `mainLanguage` and `browserLanguage` reflect captured visit languages.
- In general, string variables hold the same values shown in reporting.

## Available helpers

| Helper | Returns |
| --- | --- |
| `len(value string)` | `int` |
| `trackingField(name string)` | `string` |
| `dataBuffer(name string)` | `string` |
| `hasVisitorTag(id string)` | `bool` |
| `oneOf(value string, candidates ...string)` | `bool` |
| `containsAny(value string, substrings ...string)` | `bool` |
| `timeInRange(startHHMM int, endHHMM int, utcOffsetHours int)` | `bool` |
| `timeAtOrAfter(hhmm int, utcOffsetHours int)` | `bool` |
| `timeAtOrBefore(hhmm int, utcOffsetHours int)` | `bool` |
| `localDay(utcOffsetHours int)` | `string` |
| `strings.Contains(value, substr string)` | `bool` |
| `strings.HasPrefix(value, prefix string)` | `bool` |
| `strings.HasSuffix(value, suffix string)` | `bool` |
| `strings.ToLower(value string)` | `string` |
| `strings.ToUpper(value string)` | `string` |

## Helper notes

- `oneOf()` does exact matching against many candidate values.
- `containsAny()` does substring matching against many candidate substrings.
- Both `oneOf()` and `containsAny()` accept either inline arguments or an
  expanded `[]string`:
  - inline: `oneOf(country, "US", "CA", "AU")`
  - expanded slice: `oneOf(country, tierOne...)` where
    `tierOne := []string{"US", "CA", "AU"}`
- Use `strings.ToLower()` to normalise a value once before case-insensitive
  comparisons, rather than lowercasing repeatedly.
- `localDay()` returns a lowercase English day name: `"monday"`,
  `"tuesday"`, … `"sunday"`. Compare against lowercase literals.
- `hasVisitorTag(id)` takes the ID of a visitor-tag asset from the
  FunnelFlux UI. Only the user knows their own tag IDs. If the user hasn't
  supplied one, use a clearly-labelled placeholder (for example
  `"REPLACE_WITH_TAG_ID"`) and tell them to swap in the real ID from the
  interface.
- The time helpers (`timeInRange`, `timeAtOrAfter`, `timeAtOrBefore`,
  `localDay`) take an integer UTC hour offset — pass the `utcOffsetHours`
  variable (or a literal like `-7`, `0`, `3`). Never pass the `timezone`
  display string (`GMT+7`) to them.
- `timeInRange(start, end, offset)` uses HHMM integer times and supports
  overnight wraparound: when `end` is earlier than `start`, the range
  crosses midnight. For example, `timeInRange(2200, 600, utcOffsetHours)`
  matches 22:00 through 06:00 in the visitor's local time. No two-check
  workaround is needed.
- `trackingField(name)` reads URL key/value parameters that are defined in
  the traffic source config.
- `dataBuffer(name)` reads from the data buffer, which captures all URL
  key/value pairs across incoming redirect links, action links, and POST
  submissions — including values not defined in the traffic source. Use it
  for data submitted *after* the initial click, such as action-link params
  and form fields.
- `trackingField()` and `dataBuffer()` names must match
  `[A-Za-z0-9_ -]{1,64}`.
- Always call helpers with parentheses and a string argument:
  use `trackingField("utm_source")`, never `trackingFields["utm_source"]`.

## Allowed operators

`==`  `!=`  `<`  `>`  `<=`  `>=`  `&&`  `||`  `!`

## Allowed syntax

- `if` / `else`
- `switch`
- Local variables (`x := ...`)
- Local non-recursive functions or closures, with these constraints:
  - arguments must be typed `bool`, `int`, or `string`
  - must return exactly one `bool`, `int`, or `string` value
  - must return on every path
- Local `[]string` literals for helper inputs
- Comments with `//` or `/* */`

## Do not use

- Imports or package declarations
- Loops (`for`)
- Goroutines, `defer`, or `select`
- `append`, `make`, `new`, `panic`, `print`, `println`, `recover`,
  `delete`, or `copy`
- Maps or indexing of any kind, including `trackingFields["x"]`
- Pointers
- Unsupported packages
- Arbitrary method calls

## Good style

- Keep routing deterministic.
- Normalise an input once when you compare it repeatedly.
- Return early when a rule clearly matches.
- Use `switch` for several mutually exclusive cases.
- Use `default` only as the failure/error fallback; give "no rule matched"
  its own named route.
- Keep comments short and tied to the route they explain.

## Example: country routing

    if country == "US" {
        return "us"
    }

    if oneOf(country, "CA", "AU", "NZ") {
        return "tier_one"
    }

    return "default"

## Example: referrer block, then country tiers

    // Normalise the referrer once for case-insensitive matching.
    ref := strings.ToLower(referrer)

    // blocked: no referrer, or an unwanted / self-referral source.
    if ref == "" || strings.Contains(ref, "domain.com") {
        return "blocked"
    }

    // tier1: top-priority markets.
    if oneOf(country, "US", "CA", "GB", "AU", "NZ") {
        return "tier1"
    }

    // tier2: secondary markets.
    if oneOf(country, "DE", "FR", "NL", "SE", "NO", "DK", "CH", "IE") {
        return "tier2"
    }

    // unmatched: country resolved but matched no tier (a normal outcome).
    // A blank country (IP lookup failed) falls through to default instead.
    if country != "" {
        return "unmatched"
    }

    return "default"

## Example: case-insensitive tracking-field routing

    source := strings.ToLower(trackingField("utm_source"))

    switch {
    case source == "":
        return "missing_source"
    case source == "google_ads":
        return "google_ads"
    case strings.Contains(source, "newsletter"):
        return "newsletter"
    }

    return "default"

## Example: ISP substring blocklist

    normalizedISP := strings.ToLower(isp)
    blockedWords := []string{"amazon", "facebook", "google"}

    if containsAny(normalizedISP, blockedWords...) {
        return "blocked_isp"
    }

    return "default"

## Example: business-hours routing (visitor-local)

    // Uses the visitor's own offset, not a hardcoded one.
    workDay := oneOf(localDay(utcOffsetHours),
        "monday", "tuesday", "wednesday", "thursday", "friday")

    if workDay && timeInRange(900, 1700, utcOffsetHours) {
        return "business_hours"
    }

    return "default"

## Example: overnight local-time routing

    // end earlier than start crosses midnight: 22:00–06:00 local.
    if timeInRange(2200, 600, utcOffsetHours) {
        return "overnight"
    }

    return "default"

## Example: local closure

    isMobile := func() bool {
        return deviceType == "mobile"
    }

    isBusinessHour := func(offset int) bool {
        return timeInRange(900, 1700, offset)
    }

    if isMobile() && isBusinessHour(utcOffsetHours) {
        return "mobile_business_hours"
    }

    return "default"

## Example: submitted-form routing

    answer := strings.ToLower(dataBuffer("question_1"))
    consent := strings.ToLower(dataBuffer("consent"))

    if oneOf(answer, "a", "b") {
        return "answer_ab"
    }

    if oneOf(consent, "true", "yes", "1") {
        return "consented"
    }

    return "default"
```

</div>
