How a formula reads

A formula is one expression, worked out once for every row of its table. It's built from four kinds of name, and how a name is written decides which kind it is: a field in brackets, a function before a parenthesis, a relation or a LET name on its own. Nothing else changes what a name means, so a formula reads the same wherever you meet it.

Written asIt isExample
[Name]A field of this row[Total float]
Relation[Name]A field of the rows a relation reachesPredecessors[Finish date]
NAME(…)A functionMAX(…), WORKDAYS(…)
nameA relation, or a name you gave with LETUpstream, u

Case doesn't matter for any of them: sum(…), Sum(…) and SUM(…) are the same function, and [total float] is [Total float]. The editor rewrites what you typed in the project's spelling when you save.

Fields

A field in brackets is always the value on the row being worked out, never on another row, even inside a function that is busy with other rows. To read another row's value, name the rows first: see Reading other rows.

A field reads what its cell shows. [Status] is "Not started", "In progress" or "Complete"; [Start date] is the date the grid shows (the actual start once the task has started, otherwise the early start); a task with no duration has a blank [Duration], not 0d.

The task fields:

FieldTypeWhat it holds
[Task code], [Name], [WBS]TextThe task's ID, its name, its WBS path (levels joined by " / ")
[Type]One of Task, Milestone, LOE, Resource, WBS
[Status]One of Not started, In progress, Complete
[Duration], [Remaining duration]DurationOriginal and remaining working time
[% complete]Number, 0 to 100
[Physical % complete]Number, 0 to 100As entered; blank when none was
[% complete type], [Duration type]TextThe task's own, else the project's
[Expected finish]DateAs entered: the remaining duration runs to it
[Planned start (import)], [Planned finish (import)]DateA started activity's planned dates as imported from P6; blank otherwise
[Start date], [Finish date]DateAs the grid shows them
[Early start], [Early finish], [Late start], [Late finish]DateFrom CPM's forward and backward passes
[Actual start], [Actual finish]DateBlank until the task starts or finishes
[Total float], [Free float]Duration
[Critical], [Longest path]TRUE or FALSE
[Constraint], [Constraint date], [Secondary constraint], [Secondary constraint date]Constraint, Date
[Calendar]TextThe name of the calendar CPM dates the task on. A field read off it is the calendar's: [Calendar][Short name] is its id, [Calendar][Hours per day] its day, from this task or any other (Predecessors[Calendar][Short name], other rows)
[Driver]A taskThe predecessor that set this task's early start
[Task]A taskThis task itself
[Justification]TextWhy the task is in the schedule, as its builder wrote it

Your project's own fields work the same way. Task codes and other attributes are text, [Phase]; a task field you defined as a number or a date is one. Formula columns are fields too: name one in brackets, [Near critical], and the column that reads it updates with it.

A name with a ] in it writes it twice: [Cost [USD]]]. When the editor can't find a field, it says so and suggests the nearest: “[Finsh] isn't a field. Did you mean [Finish date]?”

Other tables have their own fields (a WBS node's [Path] and [Level], a resource's [Hourly rate], a relationship's [Lag]): see tables.

Operators

OperatorDoesExample
+ - * /Arithmetic. Works on durations too: 5d * 2 is 10d[Duration] * 1.2
^Power2 ^ 3
&Joins text; anything else is shown as text first[Task code] & " " & [Name]
= <> < <= > >=Compare two values: TRUE or FALSE[Status] <> "Complete"
AND, OR, NOTCombine TRUE and FALSE. Also work as functions: AND(a, b, c)[Critical] AND [Duration] > 20d

AND binds tighter than OR, as in most languages; use parentheses when you mix them. A comparison can't be chained: 0 < [% complete] < 100 is refused, so write [% complete] > 0 AND [% complete] < 100.

Durations

A duration is working time, written as a number with a unit: 5d, 2w, 8h, 0.5d.

d and w count in the calendar of the task the value belongs to. [Total float] < 5d is five of that task's working days: on a 10-hour calendar that is 50 hours, on a standard calendar 40. Where a duration belongs to no task (5d * 2 on its own, a measure on the project), a day is the project's: the hours per day of the calendar most tasks use.

A plain number where a duration is expected is working days of the row's calendar, so [Total float] < 5 is the same as [Total float] < 5d. That is the only time Owl turns one kind of value into another for you.

ExpressionGives
duration ± durationA duration
duration × number, duration ÷ numberA duration
duration ÷ durationA number: [Remaining duration] / [Duration]
INDAYS(duration), INHOURS(duration)A plain number of working days or hours

A duration shows in the grid in working days. Wrap it in INDAYS or INHOURS for a number you can format or divide by money.

Dates

There are no date literals: write a date with DATE, DATE(2026, 12, 1). A typed date means different things in different countries, so Owl never guesses.

You wantWrite
Working time between two datesWORKDAYS([Start date], [Finish date])
Calendar days between two datesCALDAYS([Start date], [Finish date])
A date some working time later[Finish date] + 5d, or ADDWORK([Finish date], 5d)
Today, or the data dateTODAY(), DATADATE()
Part of a dateYEAR(…), MONTH(…), DAY(…), WEEKDAY(…)
Its time of dayHOUR(…), MINUTE(…); a date at a time, DATE(2026, 12, 1, 8, 30)

Subtracting one date from another is refused: “[Finish date] - [Start date] subtracts two dates. Use WORKDAYS(…) or CALDAYS(…).” The two answers differ by every weekend and holiday, so the formula says which it means.

Adding a duration to a date steps through the task's own calendar, as CPM does: a Friday finish plus 1d on a five-day calendar is Monday.

Dates compare by the day. A task starting at 08:00 and one starting at 17:00 on the same day have equal [Start date]s. To compare to the hour, compare the hour itself, HOUR([Start date]) >= 8, or the working time between two dates: WORKDAYS(a, b) > 0h. Times are read in UTC wall clock, the clock Owl keeps schedules in.

Text

Text goes in double quotes, "Civil"; a quote inside is written twice, "6"" pipe". Comparing text ignores capitals, as Excel does: "civil" = "Civil" is TRUE.

Fields with a fixed set of values ([Status], [Type], [Constraint], a relationship's [Type]) are compared with text too, and the editor checks the text against the values: “Completed isn't a status. Did you mean Complete?”

Text never turns into a number by itself (read one out of it with VALUE), and a number never turns into TRUE or FALSE.

Looking for text inside text (CONTAINS, STARTSWITH, TEXTBEFORE, TEXTAFTER) ignores accents and other marks as well as capitals: "becancour" finds Bécancour, and Arabic is matched without its harakat. = ignores capitals only.

Blanks and errors

A row's value can be blank, or an error, instead of a value. They're different things, and Owl never turns one into 0, "" or FALSE behind your back.

BlankError
MeansNothing to work out: no baseline, no predecessors, a field not setWorking it out failed: a division by zero, ONLY on a set of three
In the gridAn empty cell. Hover for the reason!. Hover for the reason and the task it came from
In a filterMatches no condition, not even "is not". Use is emptyMatches no condition
In colorsFalls through to otherwiseFalls through to otherwise
SortingLastLast
In a guardrailThe row isn't checkedThe row is listed under Couldn't check

How they travel through a formula:

  • Arithmetic and functions pass them on. [Total float] / 2 is blank where float is blank, and an error anywhere in a calculation makes the result that error.
  • & reads a blank as empty text: [Phase] & "!" is "!" where Phase isn't set.
  • Comparisons pass them on too. [Actual finish] > DATE(2026, 6, 1) is blank, not FALSE, on an unfinished task. So [Actual finish] = BLANK() is blank on every row, never TRUE: to test for a blank, use ISBLANK.
  • IF only works out the branch it takes. IF([Duration] > 0d, [Remaining duration] / [Duration], 0) never divides by zero.
  • AND and OR stop when they know. FALSE AND x is FALSE and TRUE OR x is TRUE, even when x is blank or an error.
  • Aggregates skip blanks, but not errors. MAX(Predecessors[Finish date]) ignores a predecessor with no finish, and one predecessor whose value failed makes the whole result that error. A bad value is never quietly dropped.

On an empty list, or when every value is blank:

AggregateGives
SUM, COUNT0
ANYFALSE
EVERYTRUE
TEXTJOIN""
MIN, MAX, AVERAGE, MEDIAN, PERCENTILEBlank ("no values")

To choose what a blank or an error becomes:

You wantUse
A fallback where the value is blankCOALESCE: COALESCE([Phase], "Unassigned")
A fallback where it failedIFERROR
To test for eitherISBLANK, ISERROR
A blank on purposeBLANK(), or IF with no else: IF([Critical], [Finish date])

Naming parts with LET

A long formula reads better with its parts named. LET takes names and values, then the result:

Formula
=LET(open, FILTER(Upstream, Upstream[Status] <> "Complete"),
    COUNT(FILTER(open, open[Critical])))

How many unfinished tasks upstream of this one are critical. A name can't reuse a function's, a relation's or another LET name's: “Upstream is a relation name (Upstream); pick another name for this LET, e.g. my_upstream”.

Formulas can run over several lines; spaces and line breaks don't matter.

Common questions

Can I refer to the row above?

No. A formula never depends on the order of the rows, so there is no "previous row". To read a related row, name it: Predecessors, WBSNode, Task("A1000"). See Reading other rows.

Why does my formula give 5d when I wanted 5?

A result has the type of what it was worked out from, and a duration shows in working days. INDAYS(…) gives the plain number.

Why is -2^2 -4?

The minus applies after the power, as in mathematics. Excel gives 4. Write (-2)^2 for 4.

Are there comments?

No. Give the column a name that says what it is, and name the parts with LET.