The @vk-io/stateless-prompt package provides a lightweight way to create prompts and collect user responses using VKβs message reply feature. Unlike traditional session-based approaches, this method is completely stateless and works by having users reply to specific bot messages.
Installation
npm install @vk-io/stateless-prompt
Node.js 12.20.0 or newer is required.
How It Works
Stateless prompts work by:
Adding a unique identifier to the end of your prompt message
User replies to that message
The bot checks if the reply contains the unique identifier
If it does, your handler is called with the userβs response
This approach eliminates the need for session storage while maintaining conversational context.
Quick Start
import { VK } from 'vk-io' ;
import { StatelessPromptManager } from '@vk-io/stateless-prompt' ;
const vk = new VK ({ token: process . env . TOKEN });
// Create a prompt for name
const namePrompt = new StatelessPromptManager ({
slug: 'name' ,
handler : async ( context , next ) => {
if ( ! context . text ) {
return context . send ( 'Please reply with text to the previous message' );
}
await context . send ( `Your name is ${ context . text } ` );
}
});
// Intercept replies to our prompt
vk . updates . on ( 'message_new' , namePrompt . middlewareIntercept );
// Trigger the prompt
vk . updates . on ( 'message_new' , ( context ) => {
if ( context . text === '/signup' ) {
return context . send (
'What \' s your name? Please reply to this message. ' + namePrompt . suffix
);
}
});
vk . updates . start ();
Creating Prompts
Basic Prompt
import { StatelessPromptManager } from '@vk-io/stateless-prompt' ;
const agePrompt = new StatelessPromptManager ({
slug: 'age' , // Unique identifier for this prompt
handler : async ( context , next ) => {
const age = Number ( context . text );
if ( isNaN ( age )) {
return context . send ( 'Please enter a valid number' );
}
await context . send ( `You are ${ age } years old` );
}
});
Unique identifier for this prompt type. Used to generate the suffix.
handler
(context, next) => unknown
required
Function to handle the userβs response when they reply to your prompt.
Using the Prompt
Attach the prompt suffix to your message:
// Register the interceptor
vk . updates . on ( 'message_new' , agePrompt . middlewareIntercept );
// Send a prompt with the suffix
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/age' ) {
await context . send (
'How old are you? Please reply to this message. ' + agePrompt . suffix
);
}
});
The suffix property returns the unique identifier that should be added to your prompt message.
Multiple Prompts
Create different prompts for different purposes:
import { StatelessPromptManager } from '@vk-io/stateless-prompt' ;
// Name prompt
const namePrompt = new StatelessPromptManager ({
slug: 'name' ,
handler : async ( context ) => {
await context . send ( `Nice to meet you, ${ context . text } !` );
}
});
// Email prompt
const emailPrompt = new StatelessPromptManager ({
slug: 'email' ,
handler : async ( context ) => {
const email = context . text ;
const emailRegex = / ^ [ ^ \s@ ] + @ [ ^ \s@ ] + \. [ ^ \s@ ] + $ / ;
if ( ! emailRegex . test ( email )) {
return context . send ( 'Invalid email format' );
}
await context . send ( `Email set to: ${ email } ` );
}
});
// Phone prompt
const phonePrompt = new StatelessPromptManager ({
slug: 'phone' ,
handler : async ( context ) => {
const phone = context . text ?. replace ( / \D / g , '' );
if ( phone ?. length !== 10 ) {
return context . send ( 'Please enter a valid 10-digit phone number' );
}
await context . send ( `Phone number: ${ phone } ` );
}
});
// Register all interceptors
vk . updates . on ( 'message_new' , namePrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , emailPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , phonePrompt . middlewareIntercept );
// Use prompts
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/setname' ) {
await context . send ( 'Enter your name: ' + namePrompt . suffix );
}
if ( context . text === '/setemail' ) {
await context . send ( 'Enter your email: ' + emailPrompt . suffix );
}
if ( context . text === '/setphone' ) {
await context . send ( 'Enter your phone: ' + phonePrompt . suffix );
}
});
Use Cases
Simple Data Collection
const feedbackPrompt = new StatelessPromptManager ({
slug: 'feedback' ,
handler : async ( context ) => {
const feedback = context . text ;
// Save to database
await db . feedback . insert ({
user_id: context . senderId ,
text: feedback ,
created_at: new Date ()
});
await context . send ( 'Thank you for your feedback! π' );
}
});
vk . updates . on ( 'message_new' , feedbackPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/feedback' ) {
await context . send (
'Please share your feedback by replying to this message. ' +
feedbackPrompt . suffix
);
}
});
const reportPrompt = new StatelessPromptManager ({
slug: 'report' ,
handler : async ( context ) => {
const report = context . text ;
if ( ! report || report . length < 10 ) {
return context . send ( 'Report must be at least 10 characters long' );
}
await sendToModerators ({
reporter: context . senderId ,
content: report ,
timestamp: Date . now ()
});
await context . send ( 'Report submitted. We \' ll review it shortly.' );
}
});
vk . updates . on ( 'message_new' , reportPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/report' ) {
await context . send (
'β οΈ Report an issue: \n ' +
'Reply to this message with details. ' +
reportPrompt . suffix
);
}
});
Answer Verification
const answerPrompt = new StatelessPromptManager ({
slug: 'quiz-answer' ,
handler : async ( context ) => {
const answer = context . text ?. toLowerCase ();
const correctAnswer = 'paris' ;
if ( answer === correctAnswer ) {
await context . send ( 'β
Correct! Paris is the capital of France.' );
} else {
await context . send ( 'β Wrong. The correct answer is Paris.' );
}
}
});
vk . updates . on ( 'message_new' , answerPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/quiz' ) {
await context . send (
'β What is the capital of France? \n ' +
'Reply to this message with your answer. ' +
answerPrompt . suffix
);
}
});
Confirmation Dialogs
const confirmPrompt = new StatelessPromptManager ({
slug: 'confirm-delete' ,
handler : async ( context ) => {
const response = context . text ?. toLowerCase ();
if ( response === 'yes' || response === 'y' ) {
await deleteUserData ( context . senderId );
await context . send ( 'β
Your data has been deleted.' );
} else {
await context . send ( 'β Deletion cancelled.' );
}
}
});
vk . updates . on ( 'message_new' , confirmPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/delete' ) {
await context . send (
'β οΈ Are you sure you want to delete all your data? \n ' +
'Reply with "yes" to confirm or "no" to cancel. ' +
confirmPrompt . suffix
);
}
});
Advanced Patterns
With Validation
const passwordPrompt = new StatelessPromptManager ({
slug: 'password' ,
handler : async ( context ) => {
const password = context . text ;
// Validate password strength
if ( ! password || password . length < 8 ) {
return context . send ( 'β Password must be at least 8 characters' );
}
if ( ! / [ A-Z ] / . test ( password )) {
return context . send ( 'β Password must contain uppercase letters' );
}
if ( ! / [ 0-9 ] / . test ( password )) {
return context . send ( 'β Password must contain numbers' );
}
// Save password (hashed, of course!)
await savePassword ( context . senderId , password );
await context . send ( 'β
Password set successfully!' );
}
});
With Attachments
const photoPrompt = new StatelessPromptManager ({
slug: 'profile-photo' ,
handler : async ( context ) => {
if ( ! context . hasAttachments ( 'photo' )) {
return context . send ( 'Please attach a photo in your reply' );
}
const photo = context . getAttachments ( 'photo' )[ 0 ];
await saveProfilePhoto ( context . senderId , photo . largeSizeUrl );
await context . send ( 'β
Profile photo updated!' );
}
});
vk . updates . on ( 'message_new' , photoPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , async ( context ) => {
if ( context . text === '/setphoto' ) {
await context . send (
'Upload your profile photo by replying to this message. ' +
photoPrompt . suffix
);
}
});
Combining with Session
You can still use sessions alongside stateless prompts:
import { SessionManager } from '@vk-io/session' ;
const sessionManager = new SessionManager ();
vk . updates . on ( 'message_new' , sessionManager . middleware );
const orderPrompt = new StatelessPromptManager ({
slug: 'order-notes' ,
handler : async ( context ) => {
// Store in session
context . session . orderNotes = context . text ;
await context . send (
'Notes saved! Your order: \n ' +
`Product: ${ context . session . product } \n ` +
`Quantity: ${ context . session . quantity } \n ` +
`Notes: ${ context . session . orderNotes } `
);
}
});
Middleware Order
Always register prompt interceptors before other message handlers to ensure replies are caught correctly.
// β
Correct order
vk . updates . on ( 'message_new' , namePrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , emailPrompt . middlewareIntercept );
vk . updates . on ( 'message_new' , otherHandlers );
// β Wrong order
vk . updates . on ( 'message_new' , otherHandlers );
vk . updates . on ( 'message_new' , namePrompt . middlewareIntercept ); // Too late!
Complete Example
import { VK } from 'vk-io' ;
import { StatelessPromptManager } from '@vk-io/stateless-prompt' ;
const vk = new VK ({ token: process . env . TOKEN });
// Create prompts
const prompts = {
name: new StatelessPromptManager ({
slug: 'user-name' ,
handler : async ( context ) => {
const name = context . text ;
if ( ! name || name . length < 2 ) {
return context . send ( 'Name must be at least 2 characters' );
}
await saveUserData ( context . senderId , { name });
await context . send ( `β
Name set to: ${ name } ` );
}
}),
age: new StatelessPromptManager ({
slug: 'user-age' ,
handler : async ( context ) => {
const age = Number ( context . text );
if ( isNaN ( age ) || age < 1 || age > 120 ) {
return context . send ( 'Please enter a valid age (1-120)' );
}
await saveUserData ( context . senderId , { age });
await context . send ( `β
Age set to: ${ age } ` );
}
}),
feedback: new StatelessPromptManager ({
slug: 'feedback' ,
handler : async ( context ) => {
const feedback = context . text ;
if ( ! feedback || feedback . length < 10 ) {
return context . send ( 'Feedback must be at least 10 characters' );
}
await db . feedback . insert ({
user_id: context . senderId ,
text: feedback ,
created_at: new Date ()
});
await context . send ( 'β
Thank you for your feedback!' );
}
})
};
// Register all interceptors
for ( const prompt of Object . values ( prompts )) {
vk . updates . on ( 'message_new' , prompt . middlewareIntercept );
}
// Command handlers
vk . updates . on ( 'message_new' , async ( context ) => {
const { text } = context ;
if ( text === '/start' ) {
return context . send (
'π Welcome! \n\n ' +
'Commands: \n ' +
'/setname - Set your name \n ' +
'/setage - Set your age \n ' +
'/feedback - Send feedback \n ' +
'/profile - View your profile'
);
}
if ( text === '/setname' ) {
return context . send (
'What \' s your name? \n ' +
'Reply to this message. ' +
prompts . name . suffix
);
}
if ( text === '/setage' ) {
return context . send (
'How old are you? \n ' +
'Reply to this message. ' +
prompts . age . suffix
);
}
if ( text === '/feedback' ) {
return context . send (
'π¬ Send us your feedback! \n ' +
'Reply to this message. ' +
prompts . feedback . suffix
);
}
if ( text === '/profile' ) {
const userData = await getUserData ( context . senderId );
return context . send (
'π€ Your Profile: \n ' +
`Name: ${ userData . name || 'Not set' } \n ` +
`Age: ${ userData . age || 'Not set' } `
);
}
return context . send ( 'Unknown command. Type /start for help.' );
});
vk . updates . start (). catch ( console . error );
Advantages
No Storage Needed No need for session storage or databases to track conversation state
Simple Implementation Very straightforward to implement and understand
Works in Groups Works in both private messages and group chats
User-Friendly Uses VKβs native reply feature that users are familiar with
Limitations
Cannot handle multi-step flows easily (use @vk-io/scenes for that)
Users must reply to the specific message (not just send text)
Cannot store intermediate state between prompts
The suffix is visible in the message (though itβs just a random string)
Best Practices
Clear Instructions
Always tell users to reply to your message 'Please **reply to this message** with your answer.'
Validate Input
Always validate user input in your handler if ( ! context . text || context . text . length < 3 ) {
return context . send ( 'Invalid input' );
}
Use Unique Slugs
Give each prompt a descriptive, unique slug slug : 'user-email-verification'
Register First
Register interceptors before other handlers vk . updates . on ( 'message_new' , prompt . middlewareIntercept );
vk . updates . on ( 'message_new' , otherHandlers );
When to Use
Use @vk-io/stateless-prompt when:
You need simple one-off questions
You want to avoid session complexity
Storage/database is not available
You need group chat compatibility
Use @vk-io/scenes instead when:
You need multi-step conversations
You need to store intermediate state
You need complex branching logic
You need enter/leave handlers