How formulas work
This guide explains the PoolCloud formula engine — how it takes a pool's water readings and calculates exactly which chemicals to add and how much. For the request/response contract, see the calculate endpoint reference.
What is a formula?
A formula is a complete recipe for balancing a pool's water chemistry. Each formula is designed for a specific sanitization type (chlorine, salt water, bromine, or minerals) and contains:
- Readings — what to measure (free chlorine, pH, alkalinity, etc.)
- Target ranges — the ideal range for each reading
- Balance order — the sequence in which to adjust chemicals (order matters)
- Treatments — the chemicals and actions used to correct each reading
- Adjusters — logic that accounts for chemical interactions (e.g. pH and alkalinity affect each other)
Formulas in detail
Chlorine (chlorine_cal_hypo)
The most common formula, for standard chlorine pools using calcium hypochlorite.
Readings: Free Chlorine, Total Chlorine, pH, Total Alkalinity, Cyanuric Acid (CYA), Calcium Hardness
Balance order and treatments:
| Step | Reading | To raise (up) | To lower (down) |
|---|---|---|---|
| 1 | pH | pH Increaser, Soda Ash | pH Decreaser, Muriatic Acid |
| 2 | Total Alkalinity | Alkalinity Increaser, Baking Soda | pH Decreaser, Muriatic Acid |
| 3 | Free Chlorine | Chlorine Tablets (default), Chlorine Granules, Liquid Chlorine | Dilute Pool |
| 4 | Cyanuric Acid | Stabilizer | Dilute Pool |
| 5 | Calcium Hardness | Hardness Increaser | Dilute Pool |
| 6 | Combined Chlorine | — | Chlorine Shock |
Default target ranges:
| Reading | Target range | Notes |
|---|---|---|
| Free Chlorine (FC) | 1 – 3 ppm | Warning buffer: +2 ppm above max |
| pH | 7.2 – 7.8 | |
| Total Alkalinity (TA) | 80 – 150 ppm | Warning buffer: +20 above, +10 below |
| Cyanuric Acid (CYA) | 30 – 50 ppm | Warning buffer: ±10 ppm |
| Calcium Hardness (CH) | 175 – 225 ppm | Warning buffer: ±25 ppm. Overridden to 200–275 for plaster/concrete/pebble pools |
| Combined Chlorine (CC) | 0 – 0.1 ppm | Inferred from TC − FC. If high, triggers shock |
Adjusters: shock adjuster (infers combined chlorine from TC − FC), pH/TA adjuster (accounts for cross-effects between pH and alkalinity).
Salt water (salt)
For pools with a salt chlorine generator (SWG). Like the chlorine formula, but adds salt monitoring and raises the CYA target, since salt systems need more stabilizer.
Readings: Salt, Free Chlorine, Total Chlorine, pH, Total Alkalinity, Cyanuric Acid (CYA), Calcium Hardness
Balance order and treatments:
| Step | Reading | To raise (up) | To lower (down) |
|---|---|---|---|
| 1 | pH | pH Increaser, Soda Ash | pH Decreaser, Muriatic Acid |
| 2 | Total Alkalinity | Alkalinity Increaser, Baking Soda | pH Decreaser, Muriatic Acid |
| 3 | Free Chlorine | Salt Generator Up (default), Chlorine Granules, Dichlor | Dilute Pool |
| 4 | Cyanuric Acid | Stabilizer | Dilute Pool |
| 5 | Calcium Hardness | Hardness Increaser | Dilute Pool |
| 6 | Combined Chlorine | — | Chlorine Shock |
| 7 | Salt | Salt | Dilute Pool |
Target range overrides (vs. chlorine):
| Reading | Default target | Salt override | Reason |
|---|---|---|---|
| Cyanuric Acid (CYA) | 30 – 50 ppm | 60 – 80 ppm | Salt systems need more CYA to protect chlorine produced by the SWG |
| Salt | — | 2700 – 3500 ppm | Required for the SWG cell to produce chlorine |
All other target ranges match the chlorine formula.
Key differences from chlorine:
- The default chlorine treatment is "Salt Generator Up" — a task, not a chemical — telling the user to turn up the cell's output
- Dichlor is offered as a chlorine substitute instead of liquid chlorine
- A salt reading with its own treatment
Bromine (bromine)
For pools and hot tubs sanitized with bromine instead of chlorine.
Readings: Bromine, pH, Total Alkalinity, Cyanuric Acid (CYA), Calcium Hardness
Balance order and treatments:
| Step | Reading | To raise (up) | To lower (down) |
|---|---|---|---|
| 1 | pH | pH Increaser, Soda Ash | pH Decreaser, Muriatic Acid |
| 2 | Total Alkalinity | Alkalinity Increaser, Baking Soda | pH Decreaser, Muriatic Acid |
| 3 | Bromine | Bromine | Dilute Pool |
| 4 | Cyanuric Acid | Stabilizer | Dilute Pool |
| 5 | Calcium Hardness | Hardness Increaser | Dilute Pool |
| 6 | Combined Chlorine | — | Chlorine Shock |
Default target ranges:
| Reading | Target range |
|---|---|
| Bromine (BRO) | 3 – 5 ppm |
| pH | 7.2 – 7.8 |
| Total Alkalinity (TA) | 80 – 150 ppm |
| Cyanuric Acid (CYA) | 30 – 50 ppm |
| Calcium Hardness (CH) | 175 – 225 ppm |
Key differences:
- No Free Chlorine or Total Chlorine readings — Bromine instead
- One treatment for raising bromine (no substitutions)
- Adjusters: pH/TA only (no shock adjuster without FC/TC)
Minerals (minerals)
For pools on a mineral sanitization system (Nature2, Frog, and similar). Minerals do most of the sanitizing; only trace chlorine is needed as a backup.
Readings: Free Chlorine, Total Chlorine, pH, Total Alkalinity, Cyanuric Acid (CYA), Calcium Hardness
Balance order and treatments: identical to the chlorine formula.
Target range overrides (vs. chlorine):
| Reading | Default target | Minerals override | Reason |
|---|---|---|---|
| Free Chlorine (FC) | 1 – 3 ppm | 0.5 – 4 ppm | Mineral systems need less chlorine |
All other target ranges match the chlorine formula.
All readings
Every reading the engine understands, with its default configuration:
| ID | Name | Short name | Units | Default value | Test range | Default target | Warning buffer |
|---|---|---|---|---|---|---|---|
fc | Free Chlorine | Free Chlorine | ppm | 4 | 0 – 10 | 1 – 3 | +2 above, none below |
tc | Total Chlorine | Total Chlorine | ppm | 4 | 0 – 10 | 0 – 0.1 | — |
cc | Combined Chlorine | Combined Chlorine | ppm | 0 | 0 – 7 | 0 – 0.1 | — |
ph | pH | pH | — | 7.4 | 5 – 9 | 7.2 – 7.8 | — |
ta | Total Alkalinity | Alkalinity | ppm | 100 | 0 – 250 | 80 – 150 | +20 above, +10 below |
cya | Stabilizer (CYA) | CYA | ppm | 40 | 0 – 300 | 30 – 50 | ±10 |
ch | Calcium Hardness | Hardness | ppm | 200 | 0 – 1000 | 175 – 225 | ±25 |
salt | Salt Level | Salt | ppm | 3200 | 0 – 5000 | 2700 – 3500 | — |
bro | Bromine | Bromine | ppm | 3 | 0 – 7 | 3 – 5 | — |
copper | Copper | Copper | ppm | 1 | 0 – 7 | 0.4 – 0.7 | — |
disox | Dissolved Oxygen | Dissolved Oxygen | ppm | 1 | 0 – 15 | 6 – 7 | — |
How warning buffers work
Some readings have a warning buffer — a zone just outside the target range where the engine generates a warning instead of a treatment action. This prevents over-treating readings that are only slightly off.
For example, Free Chlorine has a target range of 1–3 ppm and a warning buffer of +2 above. If FC reads 4.5 ppm, it's above the 3 ppm max but within the 5 ppm warning ceiling (3 + 2), so the engine returns a warning rather than telling you to drain the pool.
Wall type effects on targets
For plaster, concrete, and pebble pools, the Calcium Hardness target is raised to 200–275 ppm (from the default 175–225 ppm). These surfaces are more susceptible to etching from low-calcium water.
How combined chlorine (CC) is calculated
Combined chlorine is not measured directly. The engine infers it:
CC = Total Chlorine (TC) − Free Chlorine (FC)
If CC is above the 0.1 ppm threshold, chloramines are present. The shock adjuster creates a delta that triggers a shock treatment to break them down through breakpoint chlorination.
All treatments
Chemical treatments
| ID | Name | Type | Concentration | Wait time | Description |
|---|---|---|---|---|---|
calc_hypo | Chlorine Granules | Dry chemical | 67% | 15 min | Pre-dissolve in a bucket of water and pour into pool. |
na_hclo | Liquid Chlorine | Liquid chemical | 10% | 60 min | Pour slowly in front of a return jet to help it mix. |
chlorine_tablets | Chlorine Tablets | Task | 100% | 60 min | Add tablets to your chlorinator or floaters. Adjust dials/knobs as needed. |
dichlor | Dichlor | Dry chemical | 99% | 15 min | Add directly to the pool. |
bromine | Bromine | Dry chemical | 100% | 20 min | Broadcast directly over the pool surface in small batches. |
soda_ash | Soda Ash | Dry chemical | 100% | 20 min | Broadcast over pool surface in small batches. Also slightly raises alkalinity. |
ph_increaser | pH Increaser | Dry chemical | 100% | 20 min | Broadcast over pool surface in small batches. Also slightly raises alkalinity. |
m_acid | Muriatic Acid | Liquid chemical | 31% | 20 min | Dilute 10:1 with water in an acid-resistant bucket. Turn off pump, pour around perimeter, then run pump for 5 hours. |
sodium_bisulfate | pH Decreaser | Dry chemical | 100% | 20 min | Broadcast over pool surface. Brush any that settles on the bottom. Also lowers alkalinity. |
baking_soda | Baking Soda | Dry chemical | 100% | 20 min | Broadcast over pool surface in small batches. Also slightly raises pH. |
alk_increaser | Alkalinity Increaser (engine currently emits "Alakalinity Increaser") | Dry chemical | 100% | 20 min | Broadcast over pool surface in small batches. Also slightly raises pH. |
cal_chlor | Hardness Increaser | Dry chemical | 100% | 480 min (8 h) | Broadcast calcium chloride over pool surface in small batches to avoid cloudy water. |
cya | Stabilizer | Dry chemical | 100% | 20 min | Dissolve in a 5-gallon bucket of pool water, pour into skimmer, run pump for several hours. |
salt | Salt | Dry chemical | 100% | 0 min | Broadcast evenly over pool surface. Run pump for 24 hours to dissolve. |
shock | Chlorine Shock | Dry chemical | 100% | 480 min (8 h) | Follow the manufacturer's directions. Shock at night; run the filter 8 hours; retest before swimming. |
Task treatments
Not chemicals — actions for the pool owner to take.
| ID | Name | Wait time | Description |
|---|---|---|---|
swg_up | Salt Generator Up | 0 min | Turn up the output on your salt chlorine generator. |
drain | Dilute Pool | 0 min | Partially drain and refill with fresh water. Retest afterward. |
drain_fc | Dilute Pool | 0 min | Stop adding chlorine and let it fall naturally, or partially drain and refill. |
drain_cya | Dilute Pool | 0 min | Stop adding stabilized chlorine (tablets/granules). Partially drain and refill using a hose filter. |
drain_ch | Dilute Pool | 0 min | Partially drain and refill with fresh water using a hose filter. |
Special treatments
| ID | Name | Purpose |
|---|---|---|
wait | Wait | Inserted automatically between treatments. The ounces field holds the number of minutes to wait. |
warning | Warning | Attached to readings that are slightly out of range but within the warning buffer. |
Substitutions by formula
Each formula has a default treatment (first in the list) for each reading direction. You can substitute any of the alternatives via the substitutions request field.
Chlorine and minerals
| Reading | Direction | Default | Alternatives |
|---|---|---|---|
| pH | Raise | ph_increaser | soda_ash |
| pH | Lower | sodium_bisulfate | m_acid |
| Total Alkalinity | Raise | alk_increaser | baking_soda |
| Total Alkalinity | Lower | sodium_bisulfate | m_acid |
| Free Chlorine | Raise | chlorine_tablets | calc_hypo, na_hclo |
| Free Chlorine | Lower | drain_fc | — |
| Cyanuric Acid | Raise | cya | — |
| Cyanuric Acid | Lower | drain_cya | — |
| Calcium Hardness | Raise | cal_chlor | — |
| Calcium Hardness | Lower | drain_ch | — |
| Combined Chlorine | Lower | shock | — |
Salt
| Reading | Direction | Default | Alternatives |
|---|---|---|---|
| pH | Raise | ph_increaser | soda_ash |
| pH | Lower | sodium_bisulfate | m_acid |
| Total Alkalinity | Raise | alk_increaser | baking_soda |
| Total Alkalinity | Lower | sodium_bisulfate | m_acid |
| Free Chlorine | Raise | swg_up | calc_hypo, dichlor |
| Free Chlorine | Lower | drain_fc | — |
| Cyanuric Acid | Raise | cya | — |
| Cyanuric Acid | Lower | drain_cya | — |
| Calcium Hardness | Raise | cal_chlor | — |
| Calcium Hardness | Lower | drain_ch | — |
| Combined Chlorine | Lower | shock | — |
| Salt | Raise | salt | — |
| Salt | Lower | drain | — |
Bromine
| Reading | Direction | Default | Alternatives |
|---|---|---|---|
| pH | Raise | ph_increaser | soda_ash |
| pH | Lower | sodium_bisulfate | m_acid |
| Total Alkalinity | Raise | alk_increaser | baking_soda |
| Total Alkalinity | Lower | sodium_bisulfate | m_acid |
| Bromine | Raise | bromine | — |
| Bromine | Lower | drain | — |
| Cyanuric Acid | Raise | cya | — |
| Cyanuric Acid | Lower | drain_cya | — |
| Calcium Hardness | Raise | cal_chlor | — |
| Calcium Hardness | Lower | drain_ch | — |
| Combined Chlorine | Lower | shock | — |
How the calculation engine works
When you submit readings to POST /api/calculate/, the engine runs through these steps:
1. Determine target ranges
For each reading, the engine picks the ideal range by checking, in priority order:
- Custom target levels passed in the request
- Formula-level overrides (salt sets CYA to 60–80 ppm; minerals sets FC to 0.5–4 ppm)
- Wall type adjustments (plaster/concrete/pebble → CH 200–275 ppm)
- Default reading targets
2. Calculate deltas
For each reading, compare the current value against the target range:
- Within target range — no action
- Outside target, within warning buffer — a warning, no chemical treatment
- Outside the warning buffer — a delta (distance from the midpoint of the target range)
3. Run adjusters
Adjusters handle chemical interactions between readings.
Shock adjuster (chlorine, salt, minerals): infers CC = TC − FC; if CC exceeds 0.1 ppm, creates a delta that triggers shock. This is why both FC and TC matter — the difference reveals chloramines.
pH / TA adjuster (all formulas): pH and total alkalinity are chemically linked — raising one raises the other. The adjuster accounts for the cross-effect so the engine doesn't over-correct.
4. Execute treatments in balance order
For each reading, in the formula's order:
- Check for a delta (positive = too low, raise; negative = too high, lower)
- Select the default treatment, or the substitution if one was provided
- Run the treatment's dosing function to get the amount in ounces from pool volume and delta
- Update all deltas to account for side effects
Why order matters: pH first, because it affects how effective everything else is. Alkalinity second, because it buffers pH. Chlorine or bromine next for sanitization. CYA and CH last — they change slowly and interact least.
5. Insert wait periods
The engine inserts "Wait" actions between treatments that need circulation time:
| Treatment | Wait before next |
|---|---|
| Chlorine Granules, Dichlor | 15 minutes |
| Most dry chemicals | 20 minutes |
| Liquid Chlorine, Chlorine Tablets | 60 minutes |
| Hardness Increaser, Chlorine Shock | 8 hours |
Wait actions appear in the response with type: "special", and the ounces field holds the number of minutes.
6. Combine drain actions
If several readings are dangerously high (say CYA and calcium hardness), the engine combines their individual drain treatments (drain_cya, drain_ch) into a single Dilute Pool action and sorts it to the top. One partial drain fixes several problems. When only one reading needs a drain, its specific drain_* treatment is returned as-is.
7. Append warnings
Readings within the warning buffer are appended as warning actions at the end. Their ounces field holds the delta from the target range (negative = below target, positive = above).
Target level overrides
Every reading has a default target range, overridable per request:
{
"target_levels": [
{ "id": "fc", "min": 3, "max": 5 },
{ "id": "ph", "min": 7.4, "max": 7.6 }
]
}
Useful when a pool has special requirements (commercial pools often run higher chlorine), a customer has custom ideal ranges, or a surface or sanitizer calls for different targets.
The pool object
| Field | Description | Impact |
|---|---|---|
gallons | Pool volume | Scales every chemical amount. A 20,000 gallon pool needs twice as much as a 10,000 gallon pool. |
water_type | Sanitization method | Informational — you select the formula explicitly via formula_id. Values the formulas recognize: chlorine, salt_water, bromine, minerals, copper, ozone, uv. |
wall_type | Pool surface material | plaster, concrete, and pebble raise the calcium hardness target to 200–275 ppm to prevent etching. Other recognized values: vinyl, fiberglass. |
Understanding the response
The response is an ordered list of actions — the steps to perform, in sequence.
Action types
| Type | Meaning | ounces contains |
|---|---|---|
raise | A reading is too low — add a chemical to increase it | Amount of chemical in ounces |
lower | A reading is too high — add a chemical to decrease it | Amount of chemical in ounces |
special | Shocking, draining, or waiting | Minutes to wait (wait actions), or the amount (shock/drain) |
warning | Slightly out of range, within the warning buffer | Delta from ideal (negative = too low, positive = too high) |
Treatment options
Each action includes a treatment_options array listing every available treatment for that reading; treatment is the selected one. Use IDs from treatment_options as substitutions in future requests to switch.
Example walkthrough
For a 10,000 gallon plaster chlorine pool with FC=0, pH=7.2, TA=80, CYA=0, CH=0 — the reference page's example request — the engine returns:
- Raise FC — add chlorine tablets (1 oz). Alternatives: chlorine granules, liquid chlorine
- Wait — 60 minutes for chlorine to circulate
- Raise CYA — add 52 oz stabilizer to protect chlorine from UV
- Wait — 20 minutes
- Raise CH — add 288 oz hardness increaser to protect the plaster
Each step is a separate entry in the actions array, in the order to perform them.
