Skip to content

Author a Flow and Report with Python

Write calculations and reports in Python instead of on screen. What you save is the same thing the screens produce, so you can open it afterwards in PXFLOW Studio and Report Workbench and keep editing there.

One pipelinexlab SDK authors both parts.

What you authorObjectWhat it doesSaved fileWhere it opens
Calculationpx.FlowTakes input values, calculates, and exposes results.pxflowPXFLOW Studio
Reportpx.ReportHolds body text, values, tables, and the positions that display results.pxreportReport Workbench

Both objects can sit in one Python file, and saving still writes two documents. To use one calculation in several Reports, author a single px.Flow and connect it from each Report, then vary only the Report content.

bash
pip install pipelinexlab
python
from pipelinexlab import px

Read every SDK call by its owner

The public SDK uses two levels of names. Start PipelineXLab declarations with px., then call actions on the object that owns the change:

Create it firstObject or handle returnedMethods available there
flow = px.Flow(...)Flow object flow.input(), .node(), .connect(), .result()
report = px.Report(...) or px.Report.open(...)Report object report.section(), .connect_flow(), .save()
summary = report.section(...)Section handle summarycontent, attachment, and result-position methods
connection = report.connect_flow(...)Connected Flow handle connection.bind(), .show()
client = px.Client(workspace=..., project=...)Client object client.run(), .run_many(), .run_batch()

This keeps each call explicit without putting unrelated operations into one flat px.* namespace. Do not import PipelineXLab core types as unqualified names; begin examples with from pipelinexlab import px. @px.function registers a reusable calculation target, while @px.component currently registers V1 companion descriptor metadata only. The Component decorator does not materialize a package declaration or executor, grant production install admission, or make a third-party Flow Node executable. Undecorated functions remain implementation helpers.

Component successor only

The code below is a successor V2 contract-only example, not a current production procedure. Exact ProjectScope data-path isolation is implemented. Production third-party Component activation and install admission stay blocked until Project authorization and execution-trust closure are complete. The current reader preserves the exact target as read-only unsupported. A future Canvas projection uses only the host-owned component.declarative@2 renderer; settings are declared through component-declared-settings@1, at most eight per declaration, and the host draws each control from its own registry, and package-owned rich UI belongs to component-rich-surface-declaration@1 (RFC-016). See Component container V2 and third-party Node UI.

Only in an approved successor does a package Component target keep its public package namespace:

python
from midas_civil.components import model_viewer

viewer = flow.node("model_viewer", model_viewer)

All public Python calls, including PipelineXLab-hosted surfaces, use px.Client(workspace="...", project="...") with an explicit Workspace and Project scope. The lowercase px.client() convenience is reserved for a planned host-scope contract and is not part of the current facade.

Know which document you are authoring

Python objectWhat you authorSaved documentVisual editor
px.Flowpublic inputs, Nodes, Node connections, public results.pxflowPXFLOW Studio
px.Reportsections, text, values, tables, result positions, Connected Flows.pxreportReport Workbench
A Report and a Flow are sibling documents in a Project. Author the Report in Workbench or the SDK, compose the Flow in the SDK or Studio, and connect Report values and result positions at the Flow public boundary.

Python, Report Workbench, and AI editing commands update the same Report document. They do not create separate kinds of Report.

One Flow module exports exactly one public flow object. A small Project may keep that Flow and one Report in the same .py file. Multiple Flows use separate modules and are imported where needed. In both layouts, report.connect_flow(...) creates the relationship in the Report draft; report.save() pins the exact Flow revision and persists the connection.

Author the complete path

This example creates a Flow, authors two Report values, connects those values to Flow inputs, chooses a Report result position, saves the documents, and then starts the saved connection.

python
from pipelinexlab import px


class CheckResults(px.Results):
    utilization: float


@px.function(
    key="check_resistance",
    version="1.0.0",
    description="Checks girder demand against resistance.",
)
def check_resistance(
    span: float,
    demand: float,
) -> CheckResults:
    resistance = span * 100.0
    return CheckResults(utilization=demand / resistance)


# 1. Calculation Flow
flow = px.Flow(
    "girder_check",
    label="Girder check",
    description="Checks girder demand against resistance.",
)
span = flow.input("span", float, unit="m", description="Clear span.")
demand = flow.input(
    "demand",
    float,
    unit="kN.m",
    description="Design demand.",
)
check = flow.node("resistance_check", check_resistance)
flow.connect(span, check.inputs.span)
flow.connect(demand, check.inputs.demand)
flow.result("utilization", check.results.utilization)


# 2. Report content and authored values
report = px.Report("girder_report", title="Girder review")

basis = report.section("design_basis", title="Design basis")
basis.text("purpose", "Review the governing girder check.")
report_span = basis.value("span", 12.0, unit="m")
report_demand = basis.value("demand", 860.0, unit="kN.m")

summary = report.section("check_summary", title="Check result")
utilization_slot = summary.result_slot(
    "utilization",
    label="Utilization",
)


# 3. Report–Flow connection
strength = report.connect_flow("strength_check", flow)
strength.bind("span_input", report_span, to=flow.inputs.span)
strength.bind("demand_input", report_demand, to=flow.inputs.demand)
strength.show(
    "utilization_result",
    flow.results.utilization,
    at=utilization_slot,
)


# Keep import and discovery free of save and run side effects.
if __name__ == "__main__":
    # 4. Open the session that saves and runs
    with px.Client(workspace="engineering", project="bridge_project") as client:
        # 5. Save the Report and its exact Flow reference
        report_revision = report.save()

        # 6. Start the saved connection
        run = client.run(strength)

Steps 1–5 are authoring operations. They validate and save the Flow and Report but do not perform the calculation. client.run(...) starts the calculation and returns the Run you can follow in Project Activity.

When a new Flow object and Report are authored in the same script, the authoring runner validates and saves the Flow first. It then saves the Report with the exact Flow revision and public port keys selected by the connection.

Match Python to the screen

Screen actionOwner and Python callWhat it creates
New FlowSDK constructor flow = px.Flow(...)a Flow object being authored
Place and connect Nodesflow.node(...), flow.connect(...)the Flow calculation structure
Add a Flow resultflow.result(...)a result other documents can connect to
New ReportSDK constructor report = px.Report(...)a Report object being authored
Add sectionReport object summary = report.section(...)one Section handle
Add text, value, or tableSection handle .text(), .value(), .table()Report content
Add a result positionSection handle .result_slot()a position that displays a calculation result
Connected Flows → Add FlowReport object connection = report.connect_flow(...)one Connected Flow handle
Use report inputConnected Flow handle .bind()Report value → Flow input
Show resultConnected Flow handle .show()Flow result → Report position
SaveReport object .save()a new .pxreport revision
connection RunClient object .run(connection)one Flow Run
Run selectedclient.run_many(connections)one independent Run per connection
Batch Runclient.run_batch(...)one child Run per scalar case

The variable returned by report.connect_flow(...), such as strength, is a draft connection handle. After report.save() pins and persists it, pass that handle to client.run(...).

Split the Flow and Report into separate files

Export the flow object from the Flow module:

python
# project_flows/girder_check.py
from pipelinexlab import px

flow = px.Flow(
    "girder_check",
    label="Girder check",
    description="Checks girder demand against resistance.",
)
# Declare inputs, Nodes, connections, and results here.

Import that object into the Report module:

python
# project_reports/girder_report.py
from pipelinexlab import px
from project_flows.girder_check import flow

report = px.Report("girder_report", title="Girder review")
connection = report.connect_flow("strength_check", flow)
# Bind Report values and choose result positions here.

if __name__ == "__main__":
    report.save()

Splitting files changes only the Python layout. The saved relationship remains one .pxreport connection to an exact .pxflow revision.

Run several connected Flows

Running one Report–Flow connection creates one independent Run. Pass several connections to client.run_many(...) to start them together. The Runtime observes the Project's concurrency limit, and completion order may differ from the list order.

The same Flow may appear in several connections. Each connection keeps its own input mappings, result positions, current state, and Run history.

python
strength_uls = report.connect_flow("strength_uls", flow)
strength_sls = report.connect_flow("strength_sls", flow)

report.save()
runs = client.run_many([strength_uls, strength_sls])

Project Activity shows these Runs together. Cancelling or retrying one Run does not change the other connections.

Choose one collection Run or a Batch

Connecting a typed List or Table to one Flow input sends the complete collection as one value and creates one Run. Use a Batch when the same scalar calculation must run once for each case.

python
# Leave span and demand unbound in this connection; each case supplies them.
strength_cases = report.connect_flow("strength_cases", flow)

cases = [
    {"case_key": "uls_01", "span": 12.0, "demand": 860.0},
    {"case_key": "uls_02", "span": 12.0, "demand": 920.0},
]

if __name__ == "__main__":
    report.save()
    client = px.Client(workspace="engineering", project="bridge_project")
    batch = client.run_batch(
        strength_cases,
        cases,
        case_key="case_key",
        inputs={
            "span": flow.inputs.span,
            "demand": flow.inputs.demand,
        },
    )

A Batch creates one child Run for each case_key. Child Runs may execute in parallel up to the Project limit, and each child keeps its own status and Result. Ports supplied through inputs are left unbound in the saved strength_cases connection. To project child results into a Report Table, save a result-table mapping and pass its stable key as result_mapping; completion order never defines row identity.

Put calculation dependencies inside a Flow

When Flow B needs a result from Flow A, connect them inside a parent Flow instead of relying on the order of two Report connections.

python
pipeline = px.Flow(
    "design_pipeline",
    label="Design pipeline",
    description="Runs section properties before the strength check.",
)
section = pipeline.subflow("section_properties", section_properties_flow)
strength_check = pipeline.subflow("strength_check", strength_check_flow)
pipeline.connect(
    section.results.section,
    strength_check.inputs.section,
)
pipeline.result(
    "utilization",
    strength_check.results.utilization,
)

Connect the completed parent Flow to the Report once. PXFLOW Studio and the Runtime then use the connections inside that Flow as the calculation order.

Edit a draft without saving first

Use client.read_report(...) for a selected part of an exact saved revision, or report.capture_context(reads=..., writes=...) for current unsaved blocks and inputs. Include every referenced block in reads and each edit target in writes. report.apply_edits(edits, context=context, values=..., cells=...) validates body, input, and table edits together before changing the draft. Changed references or targets cause refusal; review the current content before trying again. Persist accepted edits with report.save().

Call attachment.capture() to freeze an unsaved image's bytes for later reads and saving, even if the original file changes. This does not upload the file or provide restart recovery. Context capture does not read image bytes automatically. These APIs do not run an AI model. See the linked API reference for examples, size limits, and conflict rules.

Continue