Writing and Managing Tasks
What are tasks?
A task is one step of your analysis — load the data, clean it, train the model — packaged so oryxflow can manage it for you. Instead of a loose function plus a hand-written line to read its input file and another to write its output, you declare what a task depends on and what it produces, and the engine handles the rest: it runs upstream steps first, skips anything already computed, and hands each task its inputs already loaded. Tasks are the main object you will be interacting with. They let you:
define input dependency tasks — so you declare the pipeline order once instead of re-wiring it every run
- process data
load input data from upstream tasks — already loaded, no manual file paths
save output data for downstream tasks — so results are cached and reused, not recomputed
run tasks — the engine runs only what’s missing
load output data — fetch any result by referencing the task that made it
You write your own tasks by inheriting from one of the predefined oryxflow task formats, for example pandas dataframes saved to parquet. Picking the parent class is how you choose the output format (parquet, CSV, pickle, in-memory, …) without writing any save/load code yourself — see Targets.
class YourTask(oryxflow.tasks.TaskPqPandas):
Additional details on how to write tasks is below. To run tasks see Running Workflows.
Define Upstream Dependency Tasks
You can define input dependencies by using a @oryxflow.requires decorator which takes input tasks. You can have no, one or multiple input tasks. This may be required when the decorator shortcut does not work.
Tip
The Claude Code plugin writes this wiring for you -
ask it to “add a task that takes <Upstream>’s output” and it emits the
task class with the correct @oryxflow.requires decorator.
# no dependency
class TaskSingleInput(oryxflow.tasks.TaskPqPandas):
#[...]
# single dependency
@oryxflow.requires(TaskSingleOutput)
class TaskSingleInput(oryxflow.tasks.TaskPqPandas):
#[...]
# multiple dependencies
@oryxflow.requires({'input1':TaskSingleOutput1, 'input2':TaskSingleOutput2})
class TaskMultipleInput(oryxflow.tasks.TaskPqPandas):
#[...]
Process Data
You process data by writing a run() function. This function will take input data, process it and save output data.
class YourTask(oryxflow.tasks.TaskPqPandas):
def run(self):
# load input data
# process data
# save data
Load Input Data
Input data from upstream dependency tasks can be easily loaded in run()
# no dependency
class TaskNoInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data = pd.read_csv(oryxflow.settings.dirpath/'file.csv') # data/file.csv
# single dependency, single output
@oryxflow.requires(TaskSingleOutput)
class TaskSingleInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data = self.inputLoad()
# single dependency, multiple outputs
@oryxflow.requires(TaskMultipleOutput)
class TaskSingleInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data1, data2 = self.inputLoad() # load all outputs
# or load just one specific output by its persists name
data1 = self.inputLoad(keys='output1')
# equivalent lower-level spelling
data1 = self.input()['output1'].load()
# multiple dependencies, single output
# prefer the named-dict form: you select deps by meaningful name, not by position
@oryxflow.requires({'input1':TaskSingleOutput1, 'input2':TaskSingleOutput2})
class TaskMultipleInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data1 = self.inputLoad()['input1']
data2 = self.inputLoad()['input2']
# or
data1 = self.inputLoad(task='input1')
data2 = self.inputLoad(task='input2')
# multiple dependencies, multiple outputs
@oryxflow.requires({'input1':TaskMultipleOutput1, 'input2':TaskMultipleOutput2})
class TaskMultipleInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data = self.inputLoad(as_dict=True)
data1a = data['input1']['output1']
data1b = data['input1']['output2']
data2a = data['input2']['output1']
data2b = data['input2']['output2']
# or
data1a, data1b = self.inputLoad()["input1"]
data2a, data2b = self.inputLoad()["input2"]
# or
data1a, data1b = self.inputLoad(task='input1')
data2a, data2b = self.inputLoad(task='input2')
# multiple dependencies (positional, without a dictionary), multiple outputs
# works, but the named-dict form above is preferred — here deps are selected by
# integer position (0, 1) instead of by name
@oryxflow.requires(TaskMultipleOutput1, TaskMultipleOutput2)
class TaskMultipleInput(oryxflow.tasks.TaskPqPandas):
def run(self):
data = self.inputLoad(as_dict=True)
data1a = data[0]['output1']
data1b = data[0]['output2']
data2a = data[1]['output1']
data2b = data[1]['output2']
# or
data1a, data1b = self.inputLoad()[0]
data2a, data2b = self.inputLoad()[1]
# or
data1a, data1b = self.inputLoad(task=0)
data2a, data2b = self.inputLoad(task=1)
Load External Files
You probably want to load external data which is not the output of a task. There are a few options.
class TaskExternalData(oryxflow.tasks.TaskPqPandas):
def run(self):
import pandas as pd
# read from oryxflow data folder
data = pd.read_parquet(oryxflow.settings.dirpath/'file.pq')
# totally manual
data = pd.read_parquet('/some/folder/file.pq')
# multiple files
from d6tstack.combine_csv import CombinerCSV
def do_stuff(df):
return df
df = CombinerCSV(glob.glob('*.csv'), apply_after_read=do_stuff).to_pandas()
For more advanced options see Sharing Workflows and Outputs
Dynamic Inputs
See Dynamic Tasks
Save Output Data
Saving output data is quick and convenient. You can save a single or multiple outputs.
# quick save one output
class TaskSingleOutput(oryxflow.tasks.TaskPqPandas):
def run(self):
self.save(data_output)
# save more than one output
class TaskMultipleOutput(oryxflow.tasks.TaskPqPandas):
persists=['output1','output2'] # declare what you will save
def run(self):
self.save({'output1':data1, 'output2':data2}) # needs to match persists
persist (singular) is a backwards-compatible alias for persists; prefer persists.
When you have multiple outputs and don’t declare persists you will get raise ValueError('Save dictionary needs to consistent with Task.persist')
Where Is Output Data Saved?
Output data by default is saved in data/, you can check with
oryxflow.settings.dirpath # folder where workflow output is saved
TaskTrain().output().path # file where task output is saved
You can change where data is saved using oryxflow.set_dir('data/'). See advanced options for Sharing Workflows and Outputs
Global Data Path can be also changed by including the path parameter to the Workflow.
Changing Task Output Formats
See Targets
Running tasks
Load Output Data
Once a workflow is run and the task is complete, you can easily load its output data by referencing the task.
data = flow.outputLoad() # load default task output
data = flow.outputLoad(as_dict=True) # useful for multi output
data2 = flow.outputLoad(TaskMultipleOutput, as_dict=True) # load another task output
data2['data1']
data2['data2']
Before you load output data you need to run the workflow. See run the workflow. If a task has not been run, it will show
raise RuntimeError('Target does not exist, make sure task is complete')
RuntimeError: Target does not exist, make sure task is complete
Which load method: output().load() vs outputLoad() vs outputLoadConcat()
There are three ways to get at a task’s output, lowest- to highest-level. Prefer the highest
one that fits — most code should just use outputLoad().
task.output().load()— lowest level.output()returns the target object (or a dict of targets for a multi-persiststask), and.load()reads it. Reach for this only when you want the target itself — its.path, or a deliberate.load(). You index the target yourself, e.g.task.output()['train'].load().task.outputLoad()/flow.outputLoad()— the default; use this to fetch results. Returns the data directly: a single object for a single-persiststask, a list (or a dict withas_dict=True) for multiple outputs, and a{flow: data}dict forWorkflowMulti. It also checks the task is complete for you.flow.outputLoadConcat()— narrow and opt-in;WorkflowMultionly. Row-stacks every flow’s output into one DataFrame, tagging each flow’s rows with its params. Use it only when every flow’s output is a schema-compatible DataFrame you want combined (the iterate-and-aggregate case). It is a separate method on purpose: it collapses the per-flow{flow: data}dict into a single frame — a different operation fromoutputLoad, and one that is meaningless for non-DataFrame outputs like models. See Advanced: Dynamic Tasks.
The same three tiers exist on the input side inside run(): self.input().load() (the
target), self.inputLoad() (the data — the default), and self.inputLoadConcat() (stack a
task’s dependencies into one frame).
Loading Output Data with Parameters
If you are using parameters this is how you load outputs. Make sure you run the task with that parameter first.
params = {'default_params':{}, 'use_params':{'preprocess':True}}
flow = oryxflow.WorkflowMulti(TaskSingleOutput, params)
data = flow.outputLoad() # load default task output
data['default_params']
data['use_params']
# multi output
data2 = flow.outputLoad(TaskMultipleOutput, as_dict=True) # load another task output
data2['default_params']['data1']
data2['default_params']['data2']
data2['use_params']['data1']
data2['use_params']['data2']
Putting it all together
See full example https://github.com/oryxintel/oryxflow/blob/master/docs/example-ml.md
See real-life project template https://github.com/d6t/d6tflow-template
Advanced: task attribute overrides
persist: data items to save, see above
external: do check dependencies, good for sharing tasks without providing code
code_version: bump (str or int) when this task’s logic changes so it and everything downstream recompute; see Automatic code invalidation
keep_versions: with code_version set, keep old versions at readable .../<Task>/v<version>/ paths
target_dir: specify directory
target_ext: specify extension
save_attrib: include taskid in filename