This is a minimal Chainlit app that listens for incoming messages and echoes them back:
import chainlit as cl
@cl.on_message
async def on_message(msg: cl.Message):
print("The user sent:", msg.content)
await cl.Message(content=f"You said: {msg.content}").send()What it does
@cl.on_messageasync def on_message(msg: cl.Message):print("The user sent:", msg.content)await cl.Message(content=f"You said: {msg.content}").send()User sends:
Hello Chainlit!Server console:
The user sent: Hello Chainlit!Chat response:
You said: Hello Chainlit!Running the app
Save the code in a file such as app.py, then run:
chainlit run app.pyOpen the local URL displayed in the terminal (typically http://localhost:8000) to interact with the bot.
One thing to consider: print() is fine for quick debugging, but for production applications it's usually better to use Python's logging module so logs can be filtered, stored, and monitored more effectively.
There are actually two different uses of cl.Message in your code, and it's easy to mix them up.
Let's look at this line:
async def on_message(msg: cl.Message):and this line:
await cl.Message(content=f"You said: {msg.content}").send()
They use the same class (cl.Message) in different ways.
- msg: cl.Message — Type Hint
Here:
async def on_message(msg: cl.Message):the part after the colon is a type annotation (type hint).
It tells Python (and developers):
"The variable msg is expected to be an object of type cl.Message."
Think of it like:
name: str
age: intor
def add(a: int, b: int):Similarly:
msg: cl.Messagemeans:
msg is a Message object provided by Chainlit
This does not create a message object.
It only describes what type msg will be.
2. Where does msg come from?
When a user sends:
Hello
Chainlit automatically creates a Message object behind the scenes.
Something conceptually like:
msg = cl.Message(
content="Hello"
)Then Chainlit calls your function:
await on_message(msg)So you don't create msg.
Chainlit creates it and passes it into your handler.
3. What is inside msg?
msg is not just a string.
It is an object that contains information about the message.
Example:
print(msg.content)might output:
Hello
You can think of it as:
msg = {
"content": "Hello",
"author": "user",
"timestamp": "..."
}(not the real implementation, but conceptually similar)
So:
msg.contentextracts the text the user typed.
Now look at:
await cl.Message(
content=f"You said: {msg.content}"
).send()This is different.
Here you are actually creating a new Message object.
Like:
new_msg = cl.Message(
content="You said: Hello"
)
Then:
await new_msg.send()sends it to the chat UI.
Simple Analogy
Imagine a post office.
Incoming letter
async def on_message(msg: cl.Message):
msg is the letter that arrived.
Chainlit delivers it to you.
Outgoing letter
cl.Message(content="Thanks!")You are writing a new letter.
Then:
.send()puts it in the mailbox.
Visual Flow
User types:
"Hello"
│
▼
Chainlit creates:
msg = Message(
content="Hello"
)
│
▼
on_message(msg)
│
▼
msg.content
= "Hello"
│
▼
Create new Message:
Message(
content="You said: Hello"
)
│
▼
send()
│
▼
Displayed in chat:
"You said: Hello"So the key distinction is:
msg: cl.Message→ "This variable will contain a Message object." (type hint)
while
cl.Message(...)→ "Create a new Message object."
Those two uses look similar because they reference the same class, but one is describing a type and the other is instantiating an object from that type.