stepflow_client/flow.rs
1// Copyright 2025 DataStax Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
4// in compliance with the License. You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software distributed under the License
9// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
10// or implied. See the License for the specific language governing permissions and limitations under
11// the License.
12
13//! Flow definition types and programmatic builder API.
14//!
15//! Types are re-exported from [`stepflow_flow`] — the canonical definitions shared
16//! with the orchestrator. The [`FlowBuilder`] provides a convenient API for
17//! constructing flows programmatically.
18//!
19//! # Quick start
20//!
21//! ```rust
22//! use stepflow_client::{FlowBuilder, ValueExpr};
23//! use stepflow_flow::values::JsonPath;
24//!
25//! let mut builder = FlowBuilder::new();
26//! builder.add_step(
27//! "say_hello",
28//! "/builtin/eval",
29//! ValueExpr::object(vec![("message".to_string(), ValueExpr::workflow_input(JsonPath::parse("$.name").unwrap()))]),
30//! );
31//! let flow = builder
32//! .output(ValueExpr::step_output("say_hello"))
33//! .build()
34//! .unwrap();
35//!
36//! let json = serde_json::to_value(&flow).unwrap();
37//! ```
38
39use std::collections::HashMap;
40
41use crate::error::BuilderError;
42
43// Re-export the canonical types from stepflow-flow.
44pub use stepflow_flow::ValueExpr;
45pub use stepflow_flow::values::{JsonPath, ValueRef};
46pub use stepflow_flow::workflow::{
47 Component, ErrorAction, ExampleInput, Flow, FlowRef, FlowSchema, Step, TestCase, TestConfig,
48};
49
50// ---------------------------------------------------------------------------
51// FlowBuilder
52// ---------------------------------------------------------------------------
53
54/// Programmatically build a [`Flow`] definition.
55///
56/// # Example
57///
58/// ```rust
59/// use stepflow_client::{FlowBuilder, ValueExpr};
60///
61/// let mut builder = FlowBuilder::new();
62/// builder.add_step(
63/// "summarize",
64/// "/builtin/openai",
65/// ValueExpr::object(vec![
66/// ("model".to_string(), ValueExpr::literal(serde_json::json!("gpt-4o"))),
67/// ]),
68/// );
69/// let flow = builder
70/// .output(ValueExpr::step_output("summarize"))
71/// .build()
72/// .unwrap();
73/// ```
74#[derive(Debug, Default)]
75pub struct FlowBuilder {
76 name: Option<String>,
77 description: Option<String>,
78 version: Option<String>,
79 steps: Vec<Step>,
80 step_ids: std::collections::HashSet<String>,
81 output: Option<ValueExpr>,
82 metadata: HashMap<String, serde_json::Value>,
83}
84
85impl FlowBuilder {
86 /// Create a new, empty builder.
87 pub fn new() -> Self {
88 Self::default()
89 }
90
91 /// Set the flow name.
92 pub fn name(mut self, name: impl Into<String>) -> Self {
93 self.name = Some(name.into());
94 self
95 }
96
97 /// Set the flow description.
98 pub fn description(mut self, description: impl Into<String>) -> Self {
99 self.description = Some(description.into());
100 self
101 }
102
103 /// Set the flow version.
104 pub fn version(mut self, version: impl Into<String>) -> Self {
105 self.version = Some(version.into());
106 self
107 }
108
109 /// Add a metadata entry.
110 pub fn metadata(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
111 self.metadata.insert(key.into(), value);
112 self
113 }
114
115 /// Add a step to the flow.
116 ///
117 /// # Panics
118 ///
119 /// Panics if a step with the same ID already exists.
120 pub fn add_step(
121 &mut self,
122 id: impl Into<String>,
123 component: impl Into<String>,
124 input: impl Into<ValueExpr>,
125 ) {
126 self.try_add_step(id, component, input)
127 .unwrap_or_else(|e| panic!("{e}"));
128 }
129
130 /// Add a step to the flow, returning an error on duplicate IDs.
131 pub fn try_add_step(
132 &mut self,
133 id: impl Into<String>,
134 component: impl Into<String>,
135 input: impl Into<ValueExpr>,
136 ) -> Result<(), BuilderError> {
137 let id = id.into();
138 if self.step_ids.contains(&id) {
139 return Err(BuilderError::DuplicateStep(id));
140 }
141 self.step_ids.insert(id.clone());
142 self.steps.push(Step {
143 id,
144 component: Component::from_string(component.into()),
145 input: input.into(),
146 on_error: None,
147 must_execute: None,
148 metadata: HashMap::new(),
149 });
150 Ok(())
151 }
152
153 /// Set the flow output expression.
154 pub fn output(mut self, expr: impl Into<ValueExpr>) -> Self {
155 self.output = Some(expr.into());
156 self
157 }
158
159 /// Build the [`Flow`].
160 pub fn build(self) -> Result<Flow, BuilderError> {
161 Ok(Flow {
162 name: self.name,
163 description: self.description,
164 version: self.version,
165 steps: self.steps,
166 output: self.output.unwrap_or_else(ValueExpr::null),
167 metadata: self.metadata,
168 ..Default::default()
169 })
170 }
171}
172
173// ---------------------------------------------------------------------------
174// Tests
175// ---------------------------------------------------------------------------
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use serde_json::json;
181
182 #[test]
183 fn test_flow_builder() {
184 let mut builder = FlowBuilder::new();
185 builder.add_step("greet", "/builtin/eval", ValueExpr::null());
186 let flow = builder
187 .output(ValueExpr::step_output("greet"))
188 .build()
189 .unwrap();
190
191 assert_eq!(flow.steps.len(), 1);
192 assert_eq!(flow.steps[0].id, "greet");
193
194 let json = serde_json::to_value(&flow).unwrap();
195 assert_eq!(json["steps"][0]["id"], "greet");
196 assert_eq!(json["output"]["$step"], "greet");
197 }
198
199 #[test]
200 fn test_flow_serialization() {
201 let mut builder = FlowBuilder::new().name("test");
202 builder.add_step(
203 "step1",
204 "/builtin/eval",
205 ValueExpr::object(vec![(
206 "model".to_string(),
207 ValueExpr::literal(json!("gpt-4o")),
208 )]),
209 );
210 let flow = builder
211 .output(ValueExpr::step_output("step1"))
212 .build()
213 .unwrap();
214
215 let json = serde_json::to_value(&flow).unwrap();
216 assert_eq!(json["name"], "test");
217 assert_eq!(json["steps"][0]["component"], "/builtin/eval");
218 }
219}