Building the Graph
Connect workflow nodes via origin and forward edges, chain multiple ingests, and use returned EntityRefs to thread a graph programmatically.
The project graph is built by wiring edges between nodes when you ingest them. You can do this with the prompt-guided shape (the server infers placement) or the explicit shape (you supply node ids directly).
Reading the IngestResult to thread subsequent nodes
Every ingest call returns an IngestResult with node_id and the resolved
origin_nodes / forward_nodes as list[EntityRef]. Use the returned node_id to wire
subsequent ingests.
from optixlog_gen import OptixClient, Projects, SimulationNode, MeasurementNode
opt = OptixClient()
grating = opt.project(Projects.GRATING_COUPLER_LAB)
# Step 1: ingest the setup node (no predecessors)
setup_result = grating.ingest(
data=SimulationNode(config={"mesh": "coarse"}, solver="fdtd"),
origin_nodes=[],
forward_nodes=[],
)
setup_id = setup_result.node_id # e.g. "node-setup-abc123"
# Step 2: ingest a refinement node that originates from the setup
refine_result = grating.ingest(
data=SimulationNode(config={"mesh": "fine"}, solver="fdtd"),
origin_nodes=[setup_id],
forward_nodes=[],
)
refine_id = refine_result.node_id
# Step 3: ingest a measurement that follows the fine simulation
measure_result = grating.ingest(
data=MeasurementNode(metric="transmission", value=0.82),
origin_nodes=[refine_id],
forward_nodes=[],
)Connecting to existing nodes
If previous nodes already exist in the project, fetch their ids from the Management API before ingesting:
from optixlog import SDKClient
from optixlog.management import Management
from optixlog_gen import OptixClient, Projects, MeasurementNode
client = SDKClient(base_url="https://optixlog.leidos.com", api_key="sk-opt-...")
# Find the most recent FDTD simulation
mgmt = Management(client)
project_mgmt = mgmt.project("proj_grating_7f3a")
sim_node = project_mgmt.workflow_nodes.first(
label="SimulationRun",
has_property={"solver": "fdtd"},
)
# Ingest a measurement downstream of it
opt = OptixClient()
grating = opt.project(Projects.GRATING_COUPLER_LAB)
result = grating.ingest(
data=MeasurementNode(metric="transmission", value=0.82),
origin_nodes=[sim_node.id],
forward_nodes=[],
)
print(result.node_id, result.origin_nodes)Branching and merging
A node can have multiple origins and multiple forward nodes. Pass all relevant ids in the lists:
# Merge two upstream nodes into one analysis node
analysis_result = grating.ingest(
data=SimulationNode(config={}, solver="fdtd"),
origin_nodes=["node-sim-a", "node-sim-b"],
forward_nodes=[],
)
# Fork: one node feeds two downstream nodes (supply forward_nodes)
grating.ingest(
data=SimulationNode(config={}, solver="eme"),
origin_nodes=["node-setup-xyz"],
forward_nodes=["node-analysis-1", "node-analysis-2"],
)Using EntityRef from results
The origin_nodes and forward_nodes on IngestResult are list[EntityRef] with type,
id, and name fields. Use them to inspect what edges the server resolved:
result = grating.ingest(
data=SimulationNode(config={}, solver="fdtd"),
origin_nodes=["node-abc"],
forward_nodes=[],
)
for ref in result.origin_nodes:
print(ref.type, ref.id, ref.name)
# e.g. "workflow_node" "node-abc" "Coarse FDTD Setup"Prompt-guided chaining
When using the prompt shape, you don't specify ids — but you still get node_id back, which
you can use in subsequent explicit calls:
first = grating.ingest(
data=SimulationNode(config={"mesh": "coarse"}, solver="fdtd"),
prompt="Initial coarse FDTD simulation.",
)
# Use explicit placement for a follow-up that depends on the first
second = grating.ingest(
data=SimulationNode(config={"mesh": "fine"}, solver="fdtd"),
origin_nodes=[first.node_id],
forward_nodes=[],
)Idempotency
ingest creates a new node or updates an existing one depending on the server's matching
logic for the given node type and payload. Check result.created to know whether a new node
was created (True) or an existing one was updated (False).