Ephemeral Messages
Ephemeral messages are visible only to a specific user in a channel or thread. They’re perfect for:
Error messages and validation feedback
Help text and usage instructions
Private confirmations and status updates
User-specific data that shouldn’t clutter the channel
Different platforms handle ephemeral messages differently:
Slack : Native ephemeral messages (session-dependent, disappear on reload)
Google Chat : Native private messages (persist, only target user sees them)
Discord : No native support - must use DM fallback
Microsoft Teams : No native support - must use DM fallback
Posting Ephemeral Messages
Use thread.postEphemeral() or channel.postEphemeral():
chat . onNewMention ( async ( thread , message ) => {
// Send ephemeral message to the user who mentioned the bot
await thread . postEphemeral (
message . author ,
"Only you can see this message!" ,
{ fallbackToDM: true }
);
});
Method Signature
interface Thread {
postEphemeral (
user : string | Author ,
message : AdapterPostableMessage | CardJSXElement ,
options : PostEphemeralOptions
) : Promise < EphemeralMessage | null >;
}
interface PostEphemeralOptions {
/**
* Controls behavior when native ephemeral is not supported.
* - true: Falls back to sending a DM to the user
* - false: Returns null if native ephemeral is not supported
*/
fallbackToDM : boolean ;
}
Fallback to DM
When fallbackToDM: true, the SDK automatically sends a DM if the platform doesn’t support native ephemeral messages:
// Always send (DM fallback on Discord/Teams)
const result = await thread . postEphemeral (
user ,
"Only you can see this!" ,
{ fallbackToDM: true }
);
if ( result ?. usedFallback ) {
console . log ( "Sent as DM because platform doesn't support ephemeral" );
}
No Fallback
When fallbackToDM: false, the method returns null if native ephemeral isn’t supported:
// Only send if native ephemeral supported
const result = await thread . postEphemeral (
user ,
"Secret!" ,
{ fallbackToDM: false }
);
if ( ! result ) {
// Platform doesn't support native ephemeral
// Handle accordingly (e.g., post public message instead)
await thread . post ( "Please check your DMs for details." );
}
EphemeralMessage Response
interface EphemeralMessage {
id : string ; // Message ID (may be empty for some platforms)
threadId : string ; // Thread ID (or DM thread if fallback used)
usedFallback : boolean ; // Whether DM fallback was used
raw : unknown ; // Platform-specific raw response
}
User Parameter
Pass either a user ID string or an Author object:
// From message author
await thread . postEphemeral (
message . author ,
"Private feedback" ,
{ fallbackToDM: true }
);
// From event user
chat . onAction ( "button" , async ( event ) => {
await event . thread . postEphemeral (
event . user ,
"Action received!" ,
{ fallbackToDM: false }
);
});
// Using user ID string
await thread . postEphemeral (
"U123ABC456" , // Slack user ID
"Hello!" ,
{ fallbackToDM: true }
);
Message Content Types
Ephemeral messages support the same content types as regular messages (except streaming):
Plain Text
await thread . postEphemeral (
user ,
"This is a private message." ,
{ fallbackToDM: true }
);
Markdown
await thread . postEphemeral (
user ,
{ markdown: "**Bold** and _italic_ text" },
{ fallbackToDM: true }
);
Rich Cards
import { Card , Text , Actions , Button } from "chat" ;
await thread . postEphemeral (
user ,
< Card title = "Private Actions" >
< Text > Choose an option: </ Text >
< Actions >
< Button id = "option1" > Option 1 </ Button >
< Button id = "option2" > Option 2 </ Button >
</ Actions >
</ Card > ,
{ fallbackToDM: false }
);
Example: Error Validation
Show error messages only to the user who triggered them:
import { emoji } from "chat" ;
chat . onSlashCommand ( "/deploy" , async ( event ) => {
const branch = event . text ;
if ( ! branch ) {
await event . channel . postEphemeral (
event . user ,
` ${ emoji . warning } Usage: /deploy <branch-name>` ,
{ fallbackToDM: false }
);
return ;
}
// Validate branch
const isValid = await validateBranch ( branch );
if ( ! isValid ) {
await event . channel . postEphemeral (
event . user ,
` ${ emoji . x } Branch " ${ branch } " not found. Please check the name.` ,
{ fallbackToDM: false }
);
return ;
}
// Post public confirmation
await event . channel . post (
` ${ emoji . rocket } Deploying ${ branch } ...`
);
});
Example: Help Text
Show help information privately:
import { Card , Text , emoji } from "chat" ;
chat . onSlashCommand ( "/help" , async ( event ) => {
await event . channel . postEphemeral (
event . user ,
< Card title = { ` ${ emoji . lightbulb } Available Commands` } >
< Text style = "bold" > Bot Commands :</ Text >
< Text >
/ help - Show this help message \ n
/ status - Check system status \ n
/ deploy & lt ; branch & gt ; - Deploy a branch \ n
/ rollback - Rollback last deployment
</ Text >
</ Card > ,
{ fallbackToDM: false }
);
});
Example: Private Confirmation
Confirm actions privately while posting public status:
chat . onAction ( "approve" , async ( event ) => {
// Public confirmation
await event . thread . post (
` ${ emoji . check } Order approved by ${ event . user . userName } `
);
// Private receipt to approver
await event . thread . postEphemeral (
event . user ,
< Card title = "Approval Receipt" >
< Text > You approved order #{event. value } </ Text >
< Text style = "muted" > A confirmation email has been sent . </ Text >
</ Card > ,
{ fallbackToDM: true }
);
});
Channel Ephemeral Messages
Use channel.postEphemeral() for channel-level messages:
chat . onSlashCommand ( "/info" , async ( event ) => {
const info = await event . channel . fetchMetadata ();
await event . channel . postEphemeral (
event . user ,
`Channel: ${ info . name } \n Members: ${ info . memberCount } ` ,
{ fallbackToDM: false }
);
});
Limitations
Ephemeral messages do not support streaming. If you try to pass an AsyncIterable<string>, it will fail.
Ephemeral messages cannot be edited or deleted after posting (platform limitation).
On Slack, ephemeral messages disappear when the user reloads their app. They are session-dependent.
Complete Example
Slash command with ephemeral validation and public result:
import { Chat , Card , Text , Fields , Field , emoji } from "chat" ;
const chat = new Chat ({ /* ... */ });
chat . onSlashCommand ( "/create-ticket" , async ( event ) => {
const title = event . text ;
// Validate input (ephemeral error)
if ( ! title ) {
await event . channel . postEphemeral (
event . user ,
` ${ emoji . warning } Usage: /create-ticket <title>` ,
{ fallbackToDM: false }
);
return ;
}
if ( title . length < 5 ) {
await event . channel . postEphemeral (
event . user ,
` ${ emoji . x } Title must be at least 5 characters` ,
{ fallbackToDM: false }
);
return ;
}
// Create ticket
const ticket = await createTicket ({
title ,
createdBy: event . user . userId ,
});
// Public confirmation
await event . channel . post (
< Card title = { ` ${ emoji . check } Ticket Created` } >
< Fields >
< Field label = "ID" value = {ticket. id } />
< Field label = "Title" value = { title } />
< Field label = "Created by" value = {event.user. userName } />
</ Fields >
</ Card >
);
// Private receipt to creator
await event . channel . postEphemeral (
event . user ,
` ${ emoji . bell } You'll be notified when your ticket is updated.` ,
{ fallbackToDM: true }
);
});
Best Practices
Use ephemeral for errors and validation
Error messages should be shown only to the user who triggered them, not the entire channel.
Provide public confirmation for actions
When a user performs an action, post a public message for transparency and a private ephemeral for personal confirmation.
Consider platform differences
Remember that Slack ephemeral messages are session-dependent, while Google Chat private messages persist.
Use fallbackToDM for important messages
If the message must reach the user regardless of platform, set fallbackToDM: true.
Next Steps
Direct Messages Send persistent private messages via DM
Error Handling Handle errors in ephemeral message delivery