Reading other rows

A formula reaches other rows through relations: names like Predecessors, Upstream and Tasks that mean "these rows, seen from this one". Predecessors[Finish date] is the finish date of each of this task's predecessors. A relation follows the schedule's logic, the WBS, or the resource assignments, never the order of the rows in the grid, so the answer is the same however the grid is sorted.

A relation gives several rows, so reading a field through it gives several values: a list. A formula's result must be one value per row, so a list is always reduced, by a function that turns many values into one: MAX(Predecessors[Finish date]), COUNT(Upstream), ANY(Successors[Critical]).

The editor refuses this
=Predecessors[Finish date]

Refused: “Predecessors[Finish date] is a list (one Date per predecessor). Reduce it with MAX, MIN, …”

Relations

From a task:

RelationThe rows
Predecessors, SuccessorsThe tasks linked directly before or after this one
Upstream, DownstreamEvery task that leads into this one, or that it leads into, at any depth
DrivingPredecessors, DrivingSuccessorsOnly over links that drive (that set the successor's early start)
DrivingUpstream, DrivingDownstreamThe driving chain, at any depth
InLinks, OutLinksThe links into or out of this task. Each carries the link's own fields ([Link type], [Lag], [Link slack], [Driving link]) and the fields of the task at the far end
PredecessorLinks, SuccessorLinksThe same links as rows of the relationships table: [Type], [Lag], [Slack], [Driving], and Predecessor[…] or Successor[…]
AssignmentsThe resources assigned to this task, with their units and the resource's fields
ExpensesThis task's expenses: costs that aren't a resource's units, such as a permit or a hire
WBSNodeThis task's WBS node: one row, WBSNode[Path]
CalendarThe calendar the task is dated on: one row, Calendar[Hours per day], Calendar[Short name] (its id)
AllEvery task in the schedule

From any row: Project (the project's one row, Project[Finish date]), Relationships (every relationship), ResourceAssignments (every assignment), Expenses (every expense) and Calendars (every calendar of the project).

Formula
=COUNT(FILTER(InLinks, InLinks[Lag] < 0h))

How many of this task's links into it carry a lead.

Formula
=WORKDAYS([Finish date], Project[Finish date])

How far this task finishes before the project does.

The relations of the other tables are under tables.

Sets and lists

A relation is a set of rows. A field read through it is a list, one value per row, in the same order. Functions that take sets and lists:

You wantUse
How many rowsCOUNT(Upstream)
A total, the largest, the meanSUM, MAX, AVERAGE of a list
Whether any or every row passesANY, EVERY
Some of the rowsFILTER
The row with the largest value, not the valueARGMAX(Upstream, Upstream[Finish date])[Task code]
Exactly one row, or an errorONLY
Whether there are noneISEMPTY
Rows in both, either, or one but not the otherINTERSECT, UNION, EXCEPT

A set with one row in it is still a set: MAX(Predecessors[Finish date]) is right with one predecessor or ten. A one-row relation (WBSNode, Calendar, Project, an assignment's Task) is a single row, read directly: WBSNode[Name].

Sets have a fixed order, the order CPM dates the tasks in, then task code, so FIRST, TEXTJOIN and ties in ARGMAX always give the same answer.

Filtering

FILTER keeps the rows of a set where a condition is TRUE. The condition is about the set you're filtering, read through the same name:

Formula
=COUNT(FILTER(Predecessors, Predecessors[Status] <> "Complete"))

How many predecessors aren't complete.

Inside the condition, a bare field still means this row. That is how a filter compares other rows with the one you're on:

Formula
=COUNT(FILTER(Predecessors, Predecessors[Phase] <> [Phase]))

How many predecessors are in a different phase from this task.

A filter that keeps nothing is an empty set, not an error: COUNT of it is 0, SUM 0, MAX blank.

Excel's SUMIF and COUNTIF are a filter and then the sum: SUM(FILTER(All, All[Phase] = "Civil")[Duration]).

Lists that line up

Arithmetic and comparisons work element by element on lists from the same set, and repeat a single value across a list:

Formula
=SUM(Assignments[Budgeted units] * Assignments[Hourly rate])

Each assignment's hours times its rate, then added up: the task's budget.

Two lists from different sets don't line up, because nothing says which element goes with which:

The editor refuses this
=MAX(Predecessors[Finish date] - Successors[Start date])

Refused: the two lists come from different sets. Name the set once with LET to read two fields of it: LET(u, Upstream, FILTER(u, u[Phase] = [Phase])).

One task by its ID

Task("A1000") is one task, by its ID: Task("A1000")[Finish date]. The editor checks the ID when you save and stores the task itself, so renaming its code later doesn't break the formula.

SAME([Phase]) is every task with the same Phase as this one, this one included: SUM(SAME([Phase])[Duration]) is the total duration of the task's phase.

Columns that read themselves

A formula column can read its own value on the rows next to it in the logic, the way CPM's forward pass reads each predecessor's finish. Rows are worked out in logic order, so a predecessor's value is ready before its successors need it:

Formula column Depth
Depth = IF(ISEMPTY(Predecessors), 0, MAX(Predecessors[Depth]) + 1)

How many links lie between this task and the start of the schedule.

The rules, each checked when you save:

A column may read itself throughNot through
One step back: Predecessors, InLinks, DrivingPredecessors, or a FILTER of themUpstream or Downstream: a row's own closure leads back to itself
Or one step forward: Successors, OutLinks, DrivingSuccessorsBoth directions in one column
On WBS rows: Parent, or ChildrenA bare [Depth] on its own row

A task on a loop in the logic gets the error "on a schedule loop"; every other task still works out.

A self-reading column is arithmetic over the dates CPM already worked out. It's not CPM: a delay added up along a path ignores calendars, constraints and progress. For what CPM would do, ask CPM with a what-if.

Tables

Every formula belongs to one table and is worked out once for each of its rows. A field is the row's own; a relation's name means what it means on that table, so Tasks on a WBS node is every task under it, and on a resource every task it is assigned to.

TableWhere you write itIts fieldsIts relations
TaskA formula column; most guardrailsThe task fieldsAbove
WBS nodeA column's formula on WBS rows; a guardrail on each WBS node[Name], [Path], [Code], [Level] (1 at the top), [Start date], [Finish date]Parent, Children, Ancestors, Descendants, Tasks (every task in or under it), All
ProjectA measure; a guardrail on the project[Name], [Data date], [Start date], [Finish date], and every measureTasks, WBS, Resources, Assignments, Expenses
ResourceA resource formula; a guardrail on each resource[Name], [Resource ID], [Resource type], [Hourly rate], [Currency], [Max units per time] (the most of it at once, 1 = one full-time resource when nobody set one)Assignments, Tasks, All
AssignmentA guardrail on each assignment[Resource], [Resource ID], [Resource type] (labor, equipment or material), [Hourly rate], [Currency], [Actual units], [Remaining units], [Rate] (the default columns: what was entered, else worked out, see cost columns), [Actual units (entered)], [Remaining units (entered)], [Rate (entered)] (exactly what was entered, blank when nothing was), [Budgeted units (entered)], [Units per time (entered)] (what was entered: one of them may be blank), [Budgeted units], [Units per time] (the default columns: the budget, else units per time × the task's working hours; units per time, else budget ÷ hours), [Curve]Task (one task: Task[Start date]), All
ExpenseA guardrail on each expense[Expense ID], [Name], [Cost account], [Budget cost], [Actual cost], [Remaining cost] (the default columns: as entered, else 0 and budget less actual), [Actual cost (entered)], [Remaining cost (entered)] (exactly what was entered), [Accrual] (start, end or uniform)Task (one task: Task[Finish date]), All
RelationshipA guardrail on each relationship[Type] (FS, SS, FF, SF), [Lag], [Slack], [Driving], [Predecessor], [Successor]Predecessor, Successor (one task each), All
CalendarA guardrail on each calendar; a task reads its own as Calendar[…], and any task's off its [Calendar]: Successor[Calendar][Hours per day], Tasks[Calendar][Short name][Name], [Short name], [Hours per day] (what 1d counts on it: a night shift is one day), [Hours per week], [Workdays per week] (days of its week a shift starts on), [Holidays], [Exceptions]Tasks, Resources (those on it), All
Formula on WBS rows
=SUM(Tasks[Duration])

On a WBS row: the duration of every task in the node and below it.

Measure (on the project)
=COUNT(FILTER(Tasks, Tasks[Critical]))

A measure: how many tasks are critical.

Formula on relationships
=IF([Successor][Status] <> "Complete", [Lag] <= 0h)

A guardrail on each relationship: no lag into work that isn't complete. Relationships into completed work are blank, so they aren't checked.

Formula
=Calendar[Hours per day] <= 8 AND Calendar[Hours per week] <= 40

On a task: TRUE when its calendar's days are 8 hours or less, and its weeks 40.

Formula on calendars
=[Workdays per week] <= 6 AND COUNT(Tasks) > 0

A guardrail on each calendar: none works all seven days, and every one has a task on it. Every calendar of the project is a row, one no task is on included.

[Calendar] is the calendar's name, and a field read off it is the calendar's, so any task's calendar is one bracket away: a relationship's ends, a link's far task, every task of a set. Compare calendars by [Short name], their id: two calendars can share a name, and a renamed one keeps its id.

Formula on relationships
=Predecessor[Calendar][Workdays per week] = Successor[Calendar][Workdays per week]

A guardrail on each relationship: both ends work the same week.

Formula on WBS rows
=COUNTDISTINCT(Tasks[Calendar][Short name])

On a WBS row: how many calendars its tasks are on.

Measure (on the project)
=EVERY(FILTER(Tasks, Tasks[Type] = "Milestone")[Calendar][Workdays per week] = 7)

A measure: TRUE when every milestone is on a seven-day calendar.

Measure (on the project)
=COUNT(Calendars)

A measure: how many calendars the project has.

Your own data tables (an imported cost ledger, a delivery log) are tables too, read from any row by their formula name: SUM(FILTER(CostLedger, CostLedger[Code] = [Task code])[Amount]) adds up the ledger lines of this task. See Tables and your own data.

Columns can read each other across tables: a task column can divide by a measure, [Duration] / Project[Total duration], and a WBS formula can add up a task column, SUM(Tasks[Near critical days]). A chain that leads back to where it started is refused: “Room reads Chain to here, which reads Room.”

Common questions

Is there a VLOOKUP?

No: relations and Task("…") replace it. To read a value off a related task, name the relation (Predecessors[Finish date], WBSNode[Name], Task("A1000")[Finish date]). To find a task by some other field, filter and take the one row: ONLY(FILTER(All, All[Name] = "Pour slab"))[Finish date].

Why is Upstream[Total float] slow when Predecessors[Total float] isn't?

Upstream can hold thousands of tasks, for each of thousands of rows. The editor shows how long a formula will take on your schedule before you save it, and refuses one that would take too long, naming the expensive part. Comparing against a fixed value, or grouping with SAME(…), keeps it fast.

How do I get the successors of each predecessor?

A list of sets isn't something a formula can hold. Write a helper column on the predecessors' side (say, COUNT(Successors)), then read it through the relation: MAX(Predecessors[Successor count]).