Flows#
UDI (Unstructured Data Integration) flows provide a Python-based orchestration framework for data integration pipelines. Unlike traditional batch or streaming flows, UDI flows use a declarative pipeline definition with operators that are executed by a Python orchestrator.
- The SDK provides the following functionality to interact with UDI flows:
Creating a flow
Retrieving flows
Updating a flow
Duplicating a flow
Deleting a flow
Running jobs
Creating a Flow#
UDI flows are created by first creating an empty flow, then adding operators using the
UDIFlow.add_operator() method.
In the SDK, you can create a UDI flow from a Project object using the
Project.create_flow() method.
You must specify flow_type='udi'.
>>> new_flow = project.create_flow(name='My UDI flow', description='optional description', flow_type='udi')
>>> new_flow
UDIFlow(name='My UDI flow', description='optional description', flow_id=..., pipeline_details=...)
After creating a flow, you can add operators to build your data integration pipeline.
Note
To add operators that require data assets (like ingest_cpd_assets), you first need to upload assets to your project.
Note
After adding operators, you must call Project.update_flow()
to save the changes to the server.
Retrieving Flows#
UDI flows can be retrieved through a Project object using the
Project.flows property.
To retrieve only UDI flows, use the Project.flows.get_all() method with flow_type='udi'.
>>> project.flows
[...]
>>> udi_flows = project.flows.get_all(flow_type='udi')
>>> udi_flows
[..., UDIFlow(name='My UDI flow', description='optional description', flow_id=..., pipeline_details=...), ...]
>>> my_flow = project.flows.get(name='My UDI flow')
>>> my_flow.name
'My UDI flow'
Updating a Flow#
You can update a UDI flow’s name and description using the Project.update_flow() method.
>>> new_flow.name = 'Updated UDI Flow Name'
>>> new_flow.description = 'Updated description'
>>> response = project.update_flow(new_flow)
>>> response.status_code
200
Duplicating a Flow#
You can create a copy of an existing UDI flow using the project’s duplicate_flow() method.
This creates a new flow with the same pipeline definition but with a new name and description.
>>> udi_flow = project.flows.get(name='My UDI flow')
>>> duplicated_flow = project.duplicate_flow(udi_flow, name='Copy of My Flow', description='Duplicated flow for testing')
>>> duplicated_flow.name
'Copy of My Flow'
>>> project.delete_flow(duplicated_flow)
<Response [...]>
Note
The duplicate_flow() method creates a copy of the flow by fetching its complete definition from the API and creating a new flow with the same configuration.
Deleting a Flow#
To delete a UDI flow, use the Project.delete_flow() method.
>>> response = project.delete_flow(new_flow)
>>> response.status_code in (200, 204)
True
Running Jobs#
UDI flows can be executed as jobs. Create and run a job using the Project.create_job() method.
>>> job = project.create_job(flow=new_flow, name='My UDI Job', description='Process data with UDI pipeline')
>>> job
Job(..., name='My UDI Job', job_id=...)
>>> job_run = job.start()
>>> job_run.state
'...'
>>> project.delete_job(job)
<Response [...]>
Discovering Available Operators#
To discover all available operators and their parameters, use the
UDIFlow.operators_metadata property.
This returns a collection of OperatorMetadata entries,
each describing an operator type including required and optional parameters, parameter types, descriptions, and default values.
>>> # Get metadata for all available operators
>>> all_metadata = new_flow.operators_metadata.get_all()
>>> all_metadata
[..., OperatorMetadata(operator_type='ingest_cpd_assets', category=..., attributes=..., features=...), ...]
>>> # View all available operator types
>>> operator_types = [m.operator_type for m in all_metadata]
>>> 'ingest_cpd_assets' in operator_types
True
>>> # Get metadata for a specific operator type
>>> ingest_metadata = new_flow.operators_metadata.get(operator_type='ingest_cpd_assets')
>>> ingest_metadata.operator_type
'ingest_cpd_assets'
Custom Operators#
UDI flows support custom operators that extend the built-in functionality with user-defined Python code. Custom operators are stored in the project settings and can be listed, uploaded, and used in flows.
Listing Custom Operators#
Custom operators are project-level resources that can be used across multiple flows.
To list all custom operators registered in your project, use the
Project.custom_operators property:
>>> # List all custom operators in the project
>>> for op in project.custom_operators:
... print(f"{op.name}")
hello_world
The CustomOperators
collection provides access to all custom operators with support for filtering and iteration.
Uploading Custom Operators#
To upload a custom operator to your project, use the
Project.upload_custom_operator() method:
>>> # Upload a custom operator file (requires actual file)
>>> custom_op = project.upload_custom_operator(
... name='my_custom_operator',
... file_path='/path/to/operator.py',
... dependency='/path/to/dependencies.tar.gz' # Optional
... )
>>> custom_op.name
'my_custom_operator'
Deleting Custom Operators#
To delete a custom operator from your project, use the
Project.delete_custom_operator() method:
>>> # Delete a custom operator
>>> custom_op = project.custom_operators.get(name='my_custom_operator')
>>> project.delete_custom_operator(custom_op)
{'status_code': 200}
Using Custom Operators#
Once a custom operator is uploaded to your project, you can use it in your flow like any built-in operator:
>>> # Add a custom operator to your flow
>>> custom_op = my_flow.add_operator('my_custom_operator', param1='value1')
>>> custom_op
<BaseOperator(type='my_custom_operator', ...)>
Deployment Spaces#
UDI flows can be promoted from projects to deployment spaces for production use.
Listing Available Spaces#
Spaces are platform-level resources (similar to projects) that serve as deployment targets for flows.
To list all available deployment spaces, use the
Platform.spaces property:
>>> # List all available deployment spaces
>>> for space in platform.spaces:
... print(f"{space.name}: {space.space_id}")
Production Space: 12345-abcde-67890
Development Space: 98765-fghij-43210
>>> # Get specific space by ID
>>> prod_space = platform.spaces['12345-abcde-67890']
Promoting to a Space#
To promote a flow from a project to a deployment space, use the
UDIFlow.promote_to_space() method:
>>> # Promote flow to a deployment space
>>> promotion_response = my_flow.promote_to_space(
... target_space_id='12345-abcde-67890',
... flow_name='Production Flow',
... description='Flow promoted to production',
... duplicate_action='REPLACE' # or 'IGNORE' to skip if exists
... )
>>> promotion_response['flow_id']
'promoted-flow-id-123'
Running in a Space#
After promoting a flow to a space, you need to work with the space directly to create and run jobs.
The promotion response contains the flow_id and space_id needed to interact with the space-based flow.
Note
Space-based jobs are managed separately from project-based jobs. You should use the space’s job management APIs directly rather than mixing project and space contexts.
Execution Logs#
To retrieve execution logs for a flow run, use the
UDIFlow.get_execution_logs() method:
>>> # Get logs for a specific job run
>>> logs = my_flow.get_execution_logs(run_id='job-run-id-123')
>>> for log_entry in logs:
... print(log_entry)
[INFO] Starting flow execution...
[INFO] Processing operator: ingest_cpd_assets
[INFO] Flow execution completed successfully
Branching and Merging#
UDI flows support conditional branching and merging, allowing you to create complex data processing pipelines that route data through different processing paths based on conditions.
Creating a Branching Operator#
To create a branching operator, use the UDIFlow.add_operator()
method with operator_type='branching'. This returns a BranchingOperator
instance that allows you to add conditional branches.
>>> # Create a branching operator
>>> quality_branch = my_flow.add_operator('branching', name='Quality-Based Processing')
>>> quality_branch
BranchingOperator(name='Quality-Based Processing', branches=0)
Adding Branches#
Use the BranchingOperator.add_branch()
method to add conditional branches. Each branch requires a condition expression and can optionally have a label and merge link name.
>>> # Add a high quality branch
>>> high_quality = quality_branch.add_branch(
... condition='docq_total_words >= 100',
... label='High Quality',
... merge_link_name='high_quality_link'
... )
>>> # Add operators to the high quality branch
>>> high_quality.add_operator('chunker', chunk_size=1000, chunk_overlap=200)
<BaseOperator(type='chunker', ...)>
>>> high_quality.add_operator('embeddings')
<BaseOperator(type='embeddings', ...)>
>>> # Add a low quality branch
>>> low_quality = quality_branch.add_branch(
... condition='docq_total_words < 100',
... label='Low Quality',
... merge_link_name='low_quality_link'
... )
>>> # Add operators to the low quality branch
>>> low_quality.add_operator('doc_quality')
<BaseOperator(type='doc_quality', ...)>
>>> low_quality.add_operator('chunker', chunk_size=500, chunk_overlap=100)
<BaseOperator(type='chunker', ...)>
Merging Branches#
After branching, you can merge the branches back together using a merge operator. The merge operator combines data from multiple branches into a single stream.
>>> # Add a merge operator to combine branches
>>> merge_op = my_flow.add_operator('merge', merge_type='rows')
>>> merge_op
<BaseOperator(type='merge', ...)>
>>> # Continue processing after merge
>>> my_flow.add_operator('milvusdb_cp4d', connection=milvus_conn, collection_name='documents')
<BaseOperator(type='milvusdb_cp4d', ...)>
Nested Branching#
Branches can contain nested branching operators, allowing for complex multi-level conditional logic.
>>> # Create main branching operator
>>> main_branch = my_flow.add_operator('branching', name='Main Quality Branch')
>>> # Add high quality branch
>>> high_quality = main_branch.add_branch(condition='quality_score >= 0.8', label='High Quality')
>>> high_quality.add_operator('chunker', chunk_size=1000)
<BaseOperator(type='chunker', ...)>
>>> # Add nested branching within high quality branch
>>> content_type_branch = high_quality.add_branching(name='Content Type Branch')
>>> expense_branch = content_type_branch.add_branch(condition='type == \'expense\'')
>>> expense_branch.add_operator('embeddings')
<BaseOperator(type='embeddings', ...)>
>>> invoice_branch = content_type_branch.add_branch(condition='type == \'invoice\'')
>>> invoice_branch.add_operator('doc_quality')
<BaseOperator(type='doc_quality', ...)>
Complete Branching Example#
Here’s a complete example showing a UDI flow with branching and merging:
>>> # Create flow
>>> branching_flow = project.create_flow(name='Branching Flow', flow_type='udi')
>>> # Add initial operators
>>> ingest_op = branching_flow.add_operator('ingest_cpd_assets', data_assets=asset_ids)
>>> extract_op = branching_flow.add_operator('extract_cpd', ocr_mode='disabled')
>>> doc_quality_op = branching_flow.add_operator('doc_quality')
>>> # Create branching operator
>>> quality_branch = branching_flow.add_operator('branching', name='Quality-Based Processing')
>>> # High quality branch
>>> high_quality = quality_branch.add_branch(
... condition='docq_total_words >= 100',
... label='High Quality',
... merge_link_name='high_quality_link'
... )
>>> high_quality.add_operator('chunker', chunk_size=1000, chunk_overlap=200, chunk_type='watsonx')
<BaseOperator(type='chunker', ...)>
>>> high_quality.add_operator('embeddings', name='High Quality Embeddings')
<BaseOperator(type='embeddings', ...)>
>>> # Low quality branch
>>> low_quality = quality_branch.add_branch(
... condition='docq_total_words < 100',
... label='Low Quality',
... merge_link_name='low_quality_link'
... )
>>> low_quality.add_operator('doc_quality', name='Re-check Quality')
<BaseOperator(type='doc_quality', ...)>
>>> low_quality.add_operator('chunker', chunk_size=500, chunk_overlap=100)
<BaseOperator(type='chunker', ...)>
>>> low_quality.add_operator('embeddings', name='Low Quality Embeddings')
<BaseOperator(type='embeddings', ...)>
>>> # Merge branches
>>> merge_op = branching_flow.add_operator('merge', merge_type='rows')
>>> # Save the flow
>>> project.update_flow(branching_flow)
<Response [200]>
>>> # Clean up
>>> project.delete_flow(branching_flow)
<Response [...]>
Note
Branch conditions use feature names from upstream operators (e.g.,
docq_total_wordsfromdoc_qualityoperator)The
merge_link_nameparameter helps identify which branch data came from when mergingBranches are evaluated in the order they are added
Each branch can have its own sequence of operators
Parameter Sets and Local Parameters#
UDI flows support parameter sets and local parameters — letting you pass values into operator fields at run time rather than hardcoding them into the flow definition.
Note
Parameter sets are project-level assets shared across flows. See Parameter Sets for how to create and manage them.
Attaching a Parameter Set to a Flow#
Once you have a parameter set, attach it to a UDI flow with
UDIFlow.use_parameter_set():
>>> from ibm_watsonx_data_integration.cpd_models.parameter_set_model import ParameterType
>>> paramset = project.create_parameter_set('udi_params')
>>> paramset.add_parameter(parameter_type=ParameterType.String, name='collection_name', value='my_collection')
ParameterSet(name='udi_params', ...)
>>> project.update_parameter_set(paramset)
<Response [200]>
>>> my_flow.use_parameter_set(paramset)
UDIFlow(...)
To reference the parameter value inside an operator field, use the #ParameterSetName.parameter_name# notation:
>>> milvus_op = my_flow.add_operator('milvusdb_cp4d')
>>> milvus_op.collection_name = '#udi_params.collection_name#'
>>> project.update_flow(my_flow)
<Response [200]>
Selecting a Default Value Set#
If your parameter set has multiple value sets, set which one the flow uses by default with
UDIFlow.set_runtime_value_set():
>>> my_flow.set_runtime_value_set(parameter_set_name='udi_params', value_set_name='production')
UDIFlow(...)
Overriding a Parameter Set Value at the Flow Level#
To override a specific parameter value as a flow-level default (affects all jobs created from this flow),
use UDIFlow.set_runtime_parameter_value():
>>> my_flow.set_runtime_parameter_value(
... parameter_set_name='udi_params',
... parameter_name='collection_name',
... value='staging_collection'
... )
UDIFlow(...)
Note
To override parameter values for a specific job or job run only, pass runtime_parameters
to Project.create_job()
or Job.start()
instead. See Per-Run Overrides via Job Start below.
Adding Local Parameters#
Local parameters are flow-scoped — they exist only within a single flow and do not require a
parameter set asset. Add one with
UDIFlow.add_local_parameter():
>>> my_flow.add_local_parameter(
... parameter_type=ParameterType.String,
... name='chunk_size',
... value='1000',
... description='Default chunk size for chunker operators'
... )
UDIFlow(...)
Pass the parameter’s default value directly when adding the operator.
The engine uses configuration.parameters.local_parameters to override the value
at run time — no token placeholder is stored in the operator field:
>>> chunker_op = my_flow.add_operator('chunker', chunk_size='1000')
>>> project.update_flow(my_flow)
<Response [200]>
Overriding a Local Parameter Value at Run Time#
Use
UDIFlow.set_runtime_local_parameter()
to change a local parameter value for all subsequent job runs on this flow instance:
>>> my_flow.set_runtime_local_parameter(local_parameter_name='chunk_size', value='500')
UDIFlow(...)
Per-Run Overrides via Job Start#
To override parameter values at the job level (all runs of that job) or for a single run only,
pass runtime_parameters to
Project.create_job()
or Job.start().
This follows the same pattern used by batch and streaming flows:
>>> # Override at job level — applies to all runs of this job
>>> job = project.create_job(flow=my_flow, name='UDI Run', runtime_parameters={'udi_params.collection_name': 'prod_collection'})
>>> # Override at run level — applies to this run only
>>> job_run = job.start(runtime_parameters={'udi_params.collection_name': 'override_collection'})
>>> # Override a local parameter: bare name
>>> job_run = job.start(runtime_parameters={'chunk_size': '2000'})
Pipeline Operators#
UDI flows support various operators for data processing:
ingest_cpd_assets: Ingest assets from Cloud Pak for Data
extract_cpd: Extract data from CPD assets
chunker: Split data into chunks for processing
pii_and_hap_extract_redact: Detect and redact PII/HAP
embeddings: Generate embeddings for text data
milvusdb_cp4d: Store data in Milvus vector database
branching: Create conditional branches for routing data based on conditions
merge: Merge multiple branches back into a single stream
Use UDIFlow.operators_metadata
to discover all available operators and their parameters dynamically.
For detailed information about branching and merging, see Branching and Merging.
API Reference#
For detailed API documentation, see: