Skip to content

Conversation

@timl3136
Copy link
Member

@timl3136 timl3136 commented Oct 7, 2025

What changed?

  • Added WorkflowDefinition class with type-safe wrapper for workflow functions, including .fn, .name, and .params properties for metadata access
  • Registry now stores WorkflowDefinition internally instead of raw callables, with get_workflow() returning the typed wrapper
  • Introduced WorkflowParameter dataclass for parameter introspection (name, type hints, defaults) and @defn decorator for standalone workflow definition

Why?

  • Enables compile-time type checking via mypy and better IDE support, consistent with existing ActivityDefinition pattern
  • Enables parameter validation, automatic API documentation, dynamic workflow discovery, and better error messages

How did you test it?
unit tests

Potential risks

Release notes

Documentation Changes

@timl3136 timl3136 changed the title Add WorkflowDefinition Add WorkflowDefinition instead of callable Oct 7, 2025
@timl3136 timl3136 changed the title Add WorkflowDefinition instead of callable Register workflow via WorkflowDefinition instead of raw callable Oct 7, 2025
Copy link
Member

@natemort natemort left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one other question is whether we want to add workflow.defn, which would be analogous to activity.defn. It defines it as a Workflow type that can later be added to a Registry.

For activities this is important because we have activity classes and interfaces, where we need to define and register them separately. That isn't strictly required for Workflows, but users might find it useful. For example, if they wanted to conditionally add a WorkflowDefinition to a registry or add it to multiple registries.

cls: Optional[Type] = None,
**kwargs: Unpack[RegisterWorkflowOptions]
) -> Callable:
) -> Union[Type, Callable[[Type], Type]]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If the return type is the same as the input type, we can use generics to indicate that. We can do this as a followup.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've added a generic for this

return func(*args, **kwargs)

# Attach metadata to the function
wrapper._workflow_run = True # type: ignore
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: All of our other decorators treat the parenthesis as optional. I think there's an argument for us not doing that, but we should be consistent across all of them.

def wrapper(*args, **kwargs):
return func(*args, **kwargs)

# Attach metadata to the function
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Maybe validate the function is async

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added validation logic.



# Create a simple namespace object for the workflow decorators
class _WorkflowNamespace:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need this, I think it's fine if run is in the root of the module.

If they import like this:

from cadence import workflow

Then they'd get:

@workflow.workflow.run

This does mean that it's possible for them to do from cadence.workflow import run but I think that's fine.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks

return WorkflowDefinition(cls, name)


def run(func: Callable[..., T]) -> Callable[..., T]:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This type signature is slightly wrong. It's saying that it accepts a function with any type of inputs and returns a function with any type of inputs and the same return type as the other function. It doesn't say that they accept the same inputs.

You could use ParamSpec to represent the arguments to the Callable to enforce that they're the same, or alternatively have T represent the Callable and its arguments, rather than just its return type. Having T represent the entire Callable is probably the easiest, and can be done by adding a bound to the TypeVar.

Feel free to address later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I add a bound to TypeVar with callable.

@timl3136 timl3136 changed the title Register workflow via WorkflowDefinition instead of raw callable feat: Register workflow via WorkflowDefinition instead of raw callable Oct 23, 2025
@timl3136 timl3136 merged commit abba8c2 into cadence-workflow:main Oct 23, 2025
7 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants