When working with Chainlit’s cl.Step class, you might notice that your steps immediately show “Used [tool_name]” even while they’re still running. Here’s how to fix it.
The Problem
When manually creating steps without the async context manager, the step appears as completed immediately:
step = cl.Step(name="transcription", type="tool")
step.input = "some input"
await step.send() # Shows "Used transcription" immediately 😢
await cl.sleep(10)
step.output = "Result"
await step.update()
The Root Cause
The frontend determines the running state by checking the start and end properties. When using async with cl.Step(), these are set automatically. But with manual usage, step.start remains None, causing the frontend to interpret the step as already completed.
The Solution
Set step.start before calling send(), and step.end before the final update():
from chainlit.utils import utc_now
step = cl.Step(name="transcription", type="tool")
step.start = utc_now() # ✨ The magic line
step.input = "some input"
await step.send() # Shows "Using transcription" ✅
await cl.sleep(10)
step.end = utc_now()
step.output = "Result"
await step.update() # Now shows "Used transcription"
Why Not Use the Context Manager?
The async context manager (async with cl.Step() as step:) handles this automatically. But sometimes you can’t use it—for example, when the step lifecycle spans multiple functions or callbacks. In those cases, manual start/end management is the way to go.
TL;DR
| Property | When to Set | Effect |
|---|---|---|
step.start = utc_now() |
Before send() |
Shows “Using” |
step.end = utc_now() |
Before final update() |
Shows “Used” |
Import utc_now from chainlit.utils and you’re good to go.