Jarren said: If the log is showing that it is being posted twice, the first thing to check is if you inadvertently added the script twice. Can you post a screenshot of the Mod API script page? The second thing to check is if you inadvertently copied and pasted the code twice in the same script block. Can you also confirm which character sheet you have selected for the game? You can see that on the Game Settings page under the ' Character Sheet Template' heading. Or you can post a screenshot from any character in the game. HI Jarren, the full script is on the second post of: <a href="https://app.roll20.net/forum/post/7474530/script-call-for-testers-universal-chat-menus/?pageforid=7474530#post-7474530" rel="nofollow">https://app.roll20.net/forum/post/7474530/script-call-for-testers-universal-chat-menus/?pageforid=7474530#post-7474530</a> I copied that exactly. The area where I put the comments is below. I've added logs and a couple of them I have commented out. Note, I've changed all my log messages to be prefixed by "Tim - " as GiGs has his own commented out logs throughout his script. if ('api' === msg_orig.type && msg_orig.content.toLowerCase().startsWith('!chatmenu ')) { let msg = _.clone(msg_orig); let args = processInlinerolls(msg).split(/\s+--/); // get character and heading let parameters = args[0].split(/\s+/); const cid = parameters[1] || ''; //character id // log("Tim - Start of Chatmenu process"); if (cid === '') { sendChat(scriptName, 'No recognised character.'); // log("Tim - No recognised character 1"); return; } const who = getAttrByName(cid, 'character_name'); if (!who) { sendChat(scriptName, 'No recognised character.'); // log("Tim - No recognised character 2"); return; } // log("Tim - Past first 2 IFs"); const sender = 'character|' + cid; const caller = (getObj('player', msg.playerid) || { get: () => 'API' }).get('_displayname'); let header = parameters.slice(2).join(' ') || defaultHeader(who); // if this starts with {template: it is a rolltemplate // and if it contains CHATMENU, the menu buttons are placed there. Otherwise they are placed at the end. // get buttons let combineSections = false; // if false, defaultTemplate sections; if true, combine all sections into one args.shift(); let buttons = buildButtons(cid, who, args); log("Tim - Let Buttons"); // buttons will be an object, each item key is a section title, and each item value is an array of buttons in that section // build chat menu buttons if (buttons.hasOwnProperty('error')) { sendChat(scriptName, buttons['error'].toString()); return; } log("Tim - Past buttons.hasOwnProperty error"); let headerPrint = `&{template:default}{{name=${header}}}`; if (header.includes('template:')) { headerPrint = `&${header}`; } headerPrint = headerPrint.replace(/(\[)/g, '[[').replace(/(\])/g, ']]'); // make sure no inline rolls present log("Tim - Past make sure no inlinerolls"); let footer = extractFrom(buttons, 'footer', ''); let separator = extractFrom(buttons, 'separator', SEPARATOR); let output = ''; let sections = Object.keys(buttons); if (headerPrint.includes(MENUPLACE)) combineSections = true; for (let section of sections) { if (buttons[section].length > 0) { output += (combineSections ? `**${section.toUpperCase()}**\n` : `{{**${section}**=`); output += buttons[section].join(separator); output += (combineSections ? `\n` : `}}`); } } let print = combineSections ? headerPrint.replace(MENUPLACE, output) : headerPrint + output; print += footer ? `\n/w ${caller} ${footer}` : ''; sendChat(sender, `/w ${caller} ${print}`,null,{noarchive:true}); log("Tim - Past sendChat(sender, `/w ${caller} ${print}"); } log("Tim - chatmenu process complete");