I’m creating a simple serverless app to synchronize the state of Freshdesk tickets with Shortcut. When locally debugging, I get a response in my browser for GET ``http://localhost:10001/iframe/api/ but no subsequent requests seem to be sent when I visit freshdesk with ?dev=true. Also nothing show up in my local console.logs.
Happy to add my server.js and manifest.json (or other files) if they are helpful.
Thanks!
Maybe this isn’t the correct forum for newbie help?
Thanks in advance!
Here is my manifest.json
{
"platform-version": "3.0",
"modules": {
"common": {
"requests": {
"get_ticket_details": {},
"create_shortcut_story": {},
"get_ticket_attachments": {},
"download_attachment": {},
"upload_to_shortcut": {},
"update_ticket_story_id": {}
}
},
"support_ticket": {
"events": {
"onTicketUpdate": {
"handler": "onTicketUpdateHandler"
}
}
}
},
"engines": {
"node": "18.18.2",
"fdk": "9.7.0"
},
"app": {
"tracking_id": "9b7yd3g4i06ej7v0tnea"
}
}
Here’s my server.js
exports = {
onTicketUpdateHandler: async function(payload) {
console.log('=== TICKET UPDATE EVENT RECEIVED ===');
console.log('Event source: ', payload.event === 'onTicketUpdate' ? 'Real Freshdesk' : 'Test simulator');
console.log('Ticket ID:', payload.data?.ticket?.id);
console.log('Current Host:', payload.currentHost);
console.log('Full payload:', JSON.stringify(payload, null, 2));
console.log('Payload keys:', Object.keys(payload));
console.log('Event timestamp:', new Date().toISOString());
try {
const { oldTicket, newTicket } = this.extractTicketData(payload);
if (this.shouldCreateShortcutStory(oldTicket, newTicket)) {
console.log('send-to-shortcut tag was added to ticket:', newTicket.id);
await this.processTicketForShortcut(newTicket, payload.iparams.project_id);
} else {
console.log('Not processing ticket - conditions not met for ticket:', newTicket.id);
}
} catch (error) {
console.error('Error in onTicketUpdateHandler:', error);
}
},
extractTicketData: function(payload) {
const oldTicket = payload.data.old_ticket || {};
const newTicket = payload.data.ticket;
console.log('Old tags:', JSON.stringify(oldTicket.tags || []));
console.log('New tags:', JSON.stringify(newTicket.tags || []));
return { oldTicket, newTicket };
},
shouldCreateShortcutStory: function(oldTicket, newTicket) {
const oldTags = oldTicket.tags || [];
const newTags = newTicket.tags || [];
// If we don't have old ticket data, check if the tag exists now
const tagWasAdded = oldTicket.id ?
(!oldTags.includes('send-to-shortcut') && newTags.includes('send-to-shortcut')) :
newTags.includes('send-to-shortcut'); // For test data without old_ticket
const hasNoStoryId = !newTicket.custom_fields.shortcut_story_id || newTicket.custom_fields.shortcut_story_id === "";
console.log('=== TAG ANALYSIS ===');
console.log('Has old ticket data:', !!oldTicket.id);
console.log('Old tags:', oldTags);
console.log('New tags:', newTags);
console.log('Tag was added/exists:', tagWasAdded);
console.log('Has no story ID:', hasNoStoryId);
console.log('Should create story:', tagWasAdded && hasNoStoryId);
console.log('Custom fields:', JSON.stringify(newTicket.custom_fields, null, 2));
return tagWasAdded && hasNoStoryId;
},
processTicketForShortcut: async function(ticket, projectId) {
try {
const ticketDetails = await this.getTicketDetails(ticket.id);
const storyData = this.buildStoryData(ticketDetails, projectId);
const shortcutStory = await this.createShortcutStory(storyData);
await this.updateTicketWithStoryId(ticket.id, shortcutStory.id);
await this.processAttachments(ticket.id, shortcutStory.id);
} catch (apiError) {
console.error('API error:', apiError);
}
},
getTicketDetails: async function(ticketId) {
try {
const ticketResponse = await $request.invokeTemplate("get_ticket_details", {
context: { ticket_id: ticketId }
});
const ticketDetails = JSON.parse(ticketResponse.response);
console.log('Retrieved ticket details:', JSON.stringify(ticketDetails));
return ticketDetails;
} catch (error) {
console.log('Failed to get ticket details, using basic ticket data. Error:', error.status);
if (error.status === 404) {
console.log('Ticket ID', ticketId, 'not found - possibly test data');
}
throw error;
}
},
buildStoryData: function(ticketDetails, projectId) {
const privateNoteContent = this.getPrivateNoteContent(ticketDetails);
const storyType = this.getStoryType(ticketDetails);
return {
name: `Ticket #${ticketDetails.id}: ${ticketDetails.subject}`,
description: `${privateNoteContent}\n\n[View Ticket](https://digitalonboarding.freshdesk.com/a/tickets/${ticketDetails.id})`,
story_type: storyType,
project_id: projectId
};
},
getPrivateNoteContent: function(ticketDetails) {
if (!ticketDetails.conversations || ticketDetails.conversations.length === 0) {
return "No private note found";
}
for (let i = ticketDetails.conversations.length - 1; i >= 0; i--) {
const conversation = ticketDetails.conversations[i];
if (conversation.private) {
return conversation.body;
}
}
return "No private note found";
},
getStoryType: function(ticketDetails) {
if (!ticketDetails.custom_fields || !ticketDetails.custom_fields.shortcut_story_type) {
return "feature";
}
const storyType = ticketDetails.custom_fields.shortcut_story_type.toLowerCase();
return ["feature", "bug", "chore"].includes(storyType) ? storyType : "feature";
},
createShortcutStory: async function(storyData) {
console.log('Creating Shortcut story with data:', JSON.stringify(storyData));
const shortcutResponse = await $request.invokeTemplate("create_shortcut_story", {
body: JSON.stringify(storyData)
});
const shortcutStory = JSON.parse(shortcutResponse.response);
console.log('Created Shortcut story:', JSON.stringify(shortcutStory));
return shortcutStory;
},
updateTicketWithStoryId: async function(ticketId, storyId) {
const updateData = {
custom_fields: {
shortcut_story_id: storyId.toString()
}
};
console.log('Updating Freshdesk ticket with Shortcut story ID:', JSON.stringify(updateData));
const updateResponse = await $request.invokeTemplate("update_ticket_story_id", {
context: { ticket_id: ticketId },
body: JSON.stringify(updateData)
});
console.log('Ticket update response:', JSON.stringify(updateResponse.response));
},
processAttachments: async function(ticketId, storyId) {
const attachments = await this.getTicketAttachments(ticketId);
if (!attachments || attachments.length === 0) {
console.log('No attachments found for this ticket');
return;
}
for (const attachment of attachments) {
await this.processAttachment(attachment, storyId);
}
},
getTicketAttachments: async function(ticketId) {
const attachmentsResponse = await $request.invokeTemplate("get_ticket_attachments", {
context: { ticket_id: ticketId }
});
const attachments = JSON.parse(attachmentsResponse.response);
console.log('Found attachments:', JSON.stringify(attachments));
return attachments;
},
processAttachment: async function(attachment, storyId) {
try {
console.log(`Downloading attachment: ${attachment.name} (ID: ${attachment.id})`);
const downloadResponse = await $request.invokeTemplate("download_attachment", {
context: { attachment_id: attachment.id }
});
// Handle binary response data - FDK should provide raw binary data
let binaryData;
if (typeof downloadResponse.response === 'string') {
// If response is base64 encoded string
binaryData = new Uint8Array(Buffer.from(downloadResponse.response, 'base64'));
} else if (downloadResponse.response instanceof ArrayBuffer) {
// If response is already ArrayBuffer
binaryData = new Uint8Array(downloadResponse.response);
} else {
// Fallback: assume it's already binary data
binaryData = downloadResponse.response;
}
const formData = new FormData();
const blob = new Blob([binaryData], { type: attachment.content_type });
formData.append('file', blob, attachment.name);
console.log(`Uploading attachment ${attachment.name} to Shortcut story ${storyId}`);
const uploadResponse = await $request.invokeTemplate("upload_to_shortcut", {
context: { story_id: storyId },
formData: formData
});
console.log(`Attachment uploaded successfully: ${JSON.stringify(uploadResponse.response)}`);
} catch (attachmentError) {
console.error(`Error processing attachment ${attachment.id}:`, attachmentError);
}
}
};
Hi @Brett_Hazen,
Why does this happen?
1. Platform v3.0 doesn’t trigger backend events locally
When you visit your Freshdesk account with ?dev=true, you’re only loading the frontend iframe of your app.
However, in your app’s manifest, your logic (e.g. onTicketUpdateHandler) is inside the serverless module, not the frontend.
FDK will not send real platform events to your localhost — only the deployed app’s backend (on Freshworks infrastructure) receives them.
So:
- The browser request
GET http://localhost:10001/iframe/api/confirms your local FDK server is running. - But no event like
onTicketUpdateis sent to localhost.
Hence,console.log()inside server.js won’t fire.
2. Understanding the ?dev=true behavior
?dev=true is meant only for frontend testing (UI placeholders, modals, etc.).
It injects your iframe-based UI locally but doesn’t simulate:
- Serverless functions
- Event triggers
$requestcalls$jobsor$schedules
3. About Manifest
You don’t need "fdk": "9.7.0" inside engines.
The FDK CLI version is managed outside the app — not by the runtime itself.
4. Test vs. Live Apps
Test apps are for testing on the platform, while Live apps are published versions of the same.
Learn more here:
Introducing Test App Versions for Custom Apps
Thanks,
Debjani