Ingesting Nodes
The two ingest call shapes — prompt-guided and explicit graph — with full examples and guidance on when to use each.
PipelineProject.ingest accepts exactly two overloaded call shapes. The data argument is
always required. The second argument determines how the node is placed in the graph.
Shape 1 — data + prompt
Pass a natural-language prompt describing what was done. The server resolves where to place
the node in the graph automatically.
from optixlog import SDKClient
from optixlog.pipeline import Pipeline
from optixlog_gen import OptixClient, Projects, SimulationNode
# Using the generated typed client (recommended)
opt = OptixClient()
grating = opt.project(Projects.GRATING_COUPLER_LAB)
result = grating.ingest(
data=SimulationNode(
config={"mesh": "fine", "pml_layers": 12},
solver="fdtd",
wavelength_nm=1550.0,
),
prompt="I ran an FDTD simulation of a grating coupler at 1550 nm with a fine mesh.",
)
print(result.node_id) # the new/updated node's id
print(result.created) # True if a new node was createdWhen to use this shape: when you're logging a new piece of work and you want the server to determine placement based on existing context — useful for interactive workflows or when you don't yet know the graph structure.
Shape 2 — data + origin_nodes + forward_nodes
Explicitly specify which nodes came before (origin_nodes) and which should follow
(forward_nodes) the new node. Both lists accept node id strings.
result = grating.ingest(
data=SimulationNode(
config={"mesh": "coarse"},
solver="eme",
),
origin_nodes=["node-setup-abc123"], # this node depends on these
forward_nodes=["node-analysis-def456"], # these depend on this node
)
print(result.node_id)
print(result.origin_nodes) # list[EntityRef] — resolved origin edges
print(result.forward_nodes) # list[EntityRef] — resolved forward edgesPass empty lists when a node has no predecessors or no successors:
# Root node — no origins
result = grating.ingest(
data=SimulationNode(config={}, solver="fdtd"),
origin_nodes=[],
forward_nodes=[],
)When to use this shape: when you're building a pipeline programmatically and already know the graph topology — for example, chaining simulation steps in a CI script or connecting measured results back to their simulation run.
Using Pipeline directly (without generated bindings)
If you have not run optixlog generate, you can implement the IngestibleNode protocol
manually. Any class with a to_payload() method that returns dict[str, JSONValue] qualifies.
from optixlog import SDKClient
from optixlog.pipeline import Pipeline
class MySimNode:
def __init__(self, solver: str, wavelength: float):
self.solver = solver
self.wavelength = wavelength
def to_payload(self) -> dict:
return {"solver": self.solver, "wavelength_nm": self.wavelength}
client = SDKClient(base_url="https://optixlog.leidos.com", api_key="sk-opt-...")
pipe = Pipeline(client)
project = pipe.project("proj_grating_7f3a")
result = project.ingest(
data=MySimNode(solver="fdtd", wavelength=1550.0),
prompt="Manual FDTD simulation.",
)Do not mix prompt and explicit nodes
You cannot pass both prompt and origin_nodes/forward_nodes in the same call. There is
no matching overload for that combination and the type checker will flag it as an error.
# This is invalid — no overload matches
project.ingest(
data=node,
prompt="Some description.",
origin_nodes=["node-abc"], # ERROR
forward_nodes=[],
)IngestibleNode protocol
The IngestibleNode protocol is the structural contract all node classes satisfy:
from optixlog.pipeline import IngestibleNode
class IngestibleNode(Protocol):
def to_payload(self) -> dict[str, JSONValue]: ...to_payload() returns the node's field data as a plain dict. Generated node classes
implement this automatically. The node_type sent to the server is the class name
(type(data).__name__), which must match a NodeLabel in the project's schema.