Common mistakes when implementing IFC scheduling with ifcopenshell.api.sequence. Each anti-pattern includes the wrong code, the correct approach, and the reason.
# NEVER create scheduling entities directly
task = model.create_entity("IfcTask", Name="Foundation Work")
schedule.IsDecomposedBy[0].RelatedObjects = (task,)# ALWAYS use the API — it creates required relationships automatically
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule,
name="Foundation Work")add_task automatically creates the IfcRelNests (for subtasks) or IfcRelAssignsToControl (for root tasks) relationship. Direct entity creation leaves tasks orphaned with no connection to the schedule, producing an invalid IFC file.
# NEVER provide both — they are mutually exclusive
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule,
parent_task=parent,
name="Subtask")# Root task: use work_schedule
root_task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule,
name="Phase 1")
# Subtask: use parent_task
sub_task = ifcopenshell.api.run("sequence.add_task", model,
parent_task=root_task,
name="Subtask A")work_schedule creates a root-level task controlled by the schedule (via IfcRelAssignsToControl). parent_task creates a nested subtask (via IfcRelNests). Providing both creates conflicting relationships.
# NEVER add IfcTaskTime to parent tasks
parent = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule, name="Structural Works")
child = ifcopenshell.api.run("sequence.add_task", model,
parent_task=parent, name="Formwork")
# BAD: Adding time to parent task
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=parent)
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={"ScheduleStart": "2026-04-01", "ScheduleDuration": "P20D"})# ONLY add time data to LEAF tasks (no subtasks)
parent = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule, name="Structural Works") # Summary — no time
child = ifcopenshell.api.run("sequence.add_task", model,
parent_task=parent, name="Formwork")
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=child)
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={"ScheduleStart": "2026-04-01", "ScheduleDuration": "P5D"})In standard scheduling practice (and IFC convention), parent tasks derive their dates from child task rollup. Assigning explicit dates to parent tasks creates conflicts when cascade_schedule or recalculate_schedule runs. Summary task dates should be inferred, not set.
task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule, name="Excavation")
# BAD: task.TaskTime is None — this will crash
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=task.TaskTime, # None!
attributes={"ScheduleStart": "2026-04-01"})task = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule, name="Excavation")
# Step 1: Create the IfcTaskTime entity
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
# Step 2: Now edit it
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={"ScheduleStart": "2026-04-01", "ScheduleDuration": "P4D"})add_task creates an IfcTask with TaskTime = None. The IfcTaskTime entity must be explicitly created with add_task_time before any time attributes can be set.
from datetime import datetime, timedelta
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
# BAD: Python datetime objects cause errors in IFC4+
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={
"ScheduleStart": datetime(2026, 4, 1), # Wrong type!
"ScheduleDuration": timedelta(days=5), # Wrong type!
})tt = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
# ALWAYS use ISO 8601 strings for IFC4+
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={
"ScheduleStart": "2026-04-01", # ISO 8601 date
"ScheduleDuration": "P5D", # ISO 8601 duration
})IFC4+ uses ISO 8601 string representations for dates and durations (IfcDate, IfcDuration). Python datetime and timedelta objects are not automatically converted and will raise type errors. Use add_date_time to convert if needed.
# NEVER create circular dependencies
seq1 = ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_a, related_process=task_b)
seq2 = ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_b, related_process=task_c)
seq3 = ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_c, related_process=task_a) # Cycle!
# This WILL crash:
ifcopenshell.api.run("sequence.cascade_schedule", model, task=task_a)
# RecursionError: maximum recursion depth exceeded# ALWAYS ensure dependency graph is a DAG (directed acyclic graph)
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_a, related_process=task_b)
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_b, related_process=task_c)
# task_c has no successor back to task_a — no cycleBoth cascade_schedule and recalculate_schedule traverse the task dependency graph recursively. Cycles cause infinite recursion leading to RecursionError. Always verify your dependency network is acyclic before cascading.
# Create tasks and sequences...
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=task_a)
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={"ScheduleStart": "2026-04-01", "ScheduleDuration": "P5D"})
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_a, related_process=task_b)
# BAD: task_b.TaskTime.ScheduleStart is still None!
# Dates do NOT propagate automatically# After setting up tasks, times, and sequences:
ifcopenshell.api.run("sequence.cascade_schedule", model, task=task_a)
# NOW task_b.TaskTime.ScheduleStart is computed from task_a's finishIfcOpenShell does NOT automatically propagate dates when tasks or sequences change. You MUST explicitly call cascade_schedule to forward-propagate dates through the dependency network.
# Confusing predecessor/successor direction
# Intent: "Formwork must finish before Rebar starts"
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_rebar, # This is the PREDECESSOR
related_process=task_formwork) # This is the SUCCESSOR
# Result: Rebar must finish before Formwork — BACKWARDS!# relating_process = PREDECESSOR (must happen first)
# related_process = SUCCESSOR (happens after)
ifcopenshell.api.run("sequence.assign_sequence", model,
relating_process=task_formwork, # Predecessor
related_process=task_rebar) # SuccessorThe naming follows IFC convention: RelatingProcess is the predecessor (the process that relates TO the dependency), RelatedProcess is the successor (the process that IS related). Think: "relating causes related."
# NEVER create schedules without a valid IFC project
model = ifcopenshell.file(schema="IFC4")
# BAD: No IfcProject, no units — invalid file
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model,
name="My Schedule")
model.write("schedule.ifc") # Invalid IFC: no IfcProjectmodel = ifcopenshell.api.run("project.create_file", version="IFC4")
project = ifcopenshell.api.run("root.create_entity", model,
ifc_class="IfcProject", name="My Project")
ifcopenshell.api.run("unit.assign_unit", model)
# NOW create schedule — file has valid project context
schedule = ifcopenshell.api.run("sequence.add_work_schedule", model,
name="My Schedule")
model.write("schedule.ifc")Every valid IFC file requires an IfcProject with assigned units. IfcWorkSchedule is linked to the project via IfcRelDeclares. Without this structure, IFC validators will reject the file.
# Intent: Wall is BUILT BY the construction task
# BAD: assign_process creates an INPUT relationship — the task operates on the wall
ifcopenshell.api.run("sequence.assign_process", model,
relating_process=task,
related_object=wall)
# Creates IfcRelAssignsToProcess. get_task_outputs(task) now returns nothing,
# and the wall is treated as an input rather than a constructed output.# For a task that CONSTRUCTS or INSTALLS a product, the product is an OUTPUT
ifcopenshell.api.run("sequence.assign_product", model,
relating_product=wall,
related_object=task)
# Creates IfcRelAssignsToProduct — what get_task_outputs() reads
# assign_process is for INPUTS: products the task operates on or consumes
demolition = ifcopenshell.api.run("sequence.add_task", model,
work_schedule=schedule, name="Demolish", predefined_type="DEMOLITION")
ifcopenshell.api.run("sequence.assign_process", model,
relating_process=demolition,
related_object=wall)IFC models processes with the ICOM paradigm — Inputs, Controls, Outputs,
Mechanisms. A product that a task creates is an Output
(IfcRelAssignsToProduct); a product it operates on or consumes is an Input
(IfcRelAssignsToProcess).
The schema names the inverse attributes accordingly: IfcTask.OperatesOn points
at IfcRelAssignsToProcess, and IfcProduct.ReferencedBy points at
IfcRelAssignsToProduct. ifcopenshell.util.sequence follows that split —
get_task_outputs() reads IfcRelAssignsToProduct, get_task_inputs() reads
OperatesOn — as do the Inputs/Outputs panels in Bonsai's sequencing UI.
ALWAYS use assign_product when a construction or installation task creates
that product as an output. NEVER use assign_process to represent that
output: the task will otherwise have zero outputs.
(Note: assign_product does not express control. Control is
IfcRelAssignsToControl, a separate relationship used for cost items and
similar.)
# Assuming IFC4+ date format without checking schema
model = ifcopenshell.open("legacy_model.ifc") # Could be IFC2X3!
tt = ifcopenshell.api.run("sequence.add_task_time", model, task=task)
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=tt,
attributes={"ScheduleStart": "2026-04-01"}) # Fails on IFC2X3!model = ifcopenshell.open("model.ifc")
# ALWAYS check schema for date handling
if model.schema == "IFC2X3":
# IFC2X3 uses IfcDateAndTime entities
import datetime
dt = datetime.datetime(2026, 4, 1)
date_value = ifcopenshell.api.run("sequence.add_date_time", model, dt=dt)
# Returns an IfcDateAndTime entity
else:
# IFC4+ uses ISO 8601 strings
date_value = "2026-04-01"IFC2X3 represents dates as IfcDateAndTime entities (complex structured data). IFC4+ uses simple ISO 8601 strings. The add_date_time helper handles the conversion, but you must know which schema you're targeting.
# NEVER modify entity attributes directly
task.Name = "Updated Name"
task.Identification = "B.1"
task.TaskTime.ScheduleStart = "2026-05-01"# ALWAYS use the API functions
ifcopenshell.api.run("sequence.edit_task", model,
task=task,
attributes={"Name": "Updated Name", "Identification": "B.1"})
ifcopenshell.api.run("sequence.edit_task_time", model,
task_time=task.TaskTime,
attributes={"ScheduleStart": "2026-05-01"})Direct attribute modification bypasses ownership tracking, transaction management, and internal consistency logic (e.g., edit_task_time auto-calculates finish dates from start + duration). Always use the API for mutations.