As a pro user you can use API scripts in your games. The script Universal Chat Menus would be really useful for what you want. It lets you format custom chat menus based on the sheet you're using. Here's one that I use for Saves and Ability checks: You select token and click the macro which sends this to chat. It creates a button for each item you specify in the menu using the values from the selected token. The original script is in the link, but I use this modified version that makes the buttons automatically whisper the rolls if you are the GM, while giving players buttons that only whisper if they set their sheet to whisper. var universalChatMenu = universalChatMenu || (function () { 'use strict'; const version = '0.3.8', scriptName = 'Universal Chat Menu', lastUpdate = 1559680306108, //defaultTitle = 'Actions', defaultHeader = (who, actions = 'Actions') => `${who} ${actions} Menu`, BUTTONSYMBOL = '!', MENUPLACE = 'CHATMENU', SEPARATOR = ' ', checkInstall = () => { log('-=> ' + scriptName + ' v' + version + ' <=- [' + (new Date(lastUpdate)) + ']'); }, ch = function (c) { var entities = { '<': 'lt', '>': 'gt', "'": '#39', '@': '#64', '{': '#123', '|': '#124', '}': '#125', '[': '#91', ']': '#93', '"': 'quot', '-': 'mdash', ' ': 'nbsp' }; if (_.has(entities, c)) { return ('&' + entities[c] + ';'); } return ''; }, // build attribute name for a repeating section repname = (section, id, name) => `repeating_${section}_${id}_${name}`, // get an array of attributes on this character - handy to shorten code elsewhere. getAttributes = (cid) => { return findObjs({ _type: 'attribute', _characterid: cid }); }, // find a list of all the row ids in a repeating section. getRepeatingIDs = (cid, section, attribute) => { const repeating = getAttributes(cid) .filter((obj) => { return obj.get('name').startsWith(`repeating_${section}`) && obj.get('name').endsWith(attribute); }) .map(obj => obj.get('name').replace(`repeating_${section}_`, '').replace('_' + attribute, '').trim()); return repeating; }, checkFilter = (check, cid, prefix = '') => { // prefix is for repeating stats, supply 'repeating_${section}_${id}_' as the prefix //log('=== FILTER ===='); //log(`check: ${check} prefix: ${prefix}`); let tests = check.toString().split(','); let rules = []; let pass = 1; // if tests are passed, return 1, otherwise 0; tests.forEach(test => { // need to check it has the three results, if only one, use rule.match = 0, and rule.type = '>' let s = test.split(/[!<>=]/); let rule = {}; rule.stat = s[0]; rule.match = s.length > 1 ? s[1] : ''; rule.type = s.length > 1 ? test.replace(s[0], '').replace(s[1], '') : '#'; // # is used for no filter rules.push(rule); // rules is an array of objects. don't need a key since will just be looping through it. }); //log(`rules: ${JSON.stringify(rules)}`); // now have a set of rules can use to test the filters // will return true or false for (let rule of rules) { let value = getAttrByName(cid, `${prefix}${rule.stat}`) || ''; // if cell is empty, it is set to null switch (rule.type) { case '#': //no filter supplied, just include if attribute exists pass = (0 === +value || (typeof value === 'string' && value.trim() === '')) ? false : true; break; case '=': // if rule.match exists in the target cell; if its a number treat as perfect match. Problem: null ||0 turns empty cells into 0. if(isNaN(value)) { pass = value.toString().toLowerCase().includes(rule.match.toString().toLowerCase()); } else { pass = (+value || 0) === (+rule.match || 0); } break; case '>': pass = (+value || 0) >= (+rule.match || Infinity); break; case '<': pass = (parseInt(value, 10) || 0) < (parseInt(rule.match, 10) || 0); break; case '!': if(isNaN(value)) { pass = !value.toString().toLowerCase().includes(rule.match.toString().toLowerCase()); } else { pass = (+value || 0) !== (+rule.match || 0); } break; } if (!pass) break; } // at this point, pass will be 0 or 1, true or false; return pass; }, buildButtons = (cid, who, args, pid) => { let parameters = {}; let title = ''; parameters[title] = []; let settings_allowed = ['title', 'footer', 'separator']; for (let arg of args) { let settings = arg.split(':'); let setting = settings[0].toLowerCase(); let setting_details = settings.length > 0 ? settings[1] : ''; if (settings_allowed.includes(setting)) { // this is a config setting; only one supported right now is title // if new title, create an empty array in parameters to hold the following attributes. switch (setting) { case 'title': title = setting_details; // dont need defaulttitle here any more //|| defaultTitle; if (!parameters.hasOwnProperty(title)) parameters[title] = []; break; case 'footer': case 'separator': parameters[setting] = setting_details; break; } } else if (arg.toLowerCase().startsWith('repeating_')) { // this is a repeating section, need to split on "|" const repeatingAttribute = arg.split('|'); if (repeatingAttribute.length < 3) { // not valid, skip it parameters['error'] = 'Error in Repeating Attributue: ' + arg; break; } const section = repeatingAttribute[0].replace('repeating_', '').trim(); //repeating section name const display = repeatingAttribute[1].trim() || 'N/A'; //display name let button = repeatingAttribute[2].trim() || 'N/A'; let buttonAttr = getButtonType(button); if (buttonAttr) button = button.split(BUTTONSYMBOL)[0].trim(); if (display === 'N/A' || button === 'N/A') { // this will likely never trigger, it just wont print that section. parameters['error'] = 'Error in Repeating Attributue: ' + arg; break; } let repeating = getRepeatingIDs(cid, section, display); let checkAttribute = repeatingAttribute.length > 3 ? repeatingAttribute.slice(3) : []; // build chat menu buttons repeating.forEach(id => { let showButton = 1; if (checkAttribute.length > 0) { showButton = checkFilter(checkAttribute,cid, `repeating_${section}_${id}_`); } if (showButton > 0) { parameters[title].push( getButtonCode(getAttrByName(cid, repname(section, id, display)), who, repname(section, id, button), pid, buttonAttr)); } }); } else { // this is a non-repeating set of attributes, split on | and then , let singleAttributes = arg.split('|'); singleAttributes.forEach(attr => { let attr_array = attr.split(','); let label = attr_array[0]; let button = attr_array[1] || label; let check = attr_array.length > 2 ? attr_array.slice(2) : []; let buttonAttr = getButtonType(button); if (buttonAttr) button = button.split(BUTTONSYMBOL)[0].trim(); // if check doesnt exist, or if it does exist but has a value of zero, print it. Otherwise don';'t let pass = 1; if(check.length > 0) { //log(`${label}: ${check}`); pass = checkFilter(check,cid,''); } if (pass) { parameters[title].push( getButtonCode(label, who, button, pid, buttonAttr)); } }); } } return parameters; }, getButtonType = (button) => button.split(BUTTONSYMBOL).length > 1, getButtonCode = (label, who, button, cid, attribute = false) => { let code = `[${label}](`; if (attribute) { code += `!&#13;/w gm &amp;${ch('{')}template:default${ch('}')}${ch('{')}${ch('{')}name=${ch('@')}${ch('{')}selected|character_name${ch('}')} ${label}${ch('}')}${ch('}')}${ch('{')}${ch('{')}=${ch('@')}${ch('{')}selected${ch('|')}${button}${ch('}')}${ch('}')}${ch('}')})`; } else if (playerIsGM(cid)){ code += `&#96;/w gm &#37;{${who}|${button}})`; } else { code += `&#96;&#37;{${who}|${button}})`; } return code; }, extractFrom = (buttons, field, fallback) => { let found = fallback; if (buttons.hasOwnProperty(field)) { found = buttons[field]; delete buttons[field]; } return found; }, handleInput = (msg) => { // copy the body of your on(chat:message) event to this function if ('api' === msg.type && msg.content.toLowerCase().startsWith('!chatmenu ')) { let args = msg.content.split(/\s+--/); // get character and heading let parameters = args[0].split(/\s+/); const cid = parameters[1] || ''; //character id if (cid === '') { sendChat(scriptName, 'No recognised character.'); return; } const who = getAttrByName(cid, 'character_name'); if (!who) { sendChat(scriptName, 'No recognised character.'); return; } 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, msg.playerid); // 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; } let headerPrint = `&{template:default}{{name=${header}}}`; if (header.includes('template:')) { headerPrint = `&${header}`; } headerPrint = headerPrint.replace(/(\[)/g, '[[').replace(/(\])/g, ']]'); // make sure no inline rolls present 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}`); } }, registerEventHandlers = () => { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready', () => { 'use strict'; universalChatMenu.CheckInstall(); universalChatMenu.RegisterEventHandlers(); }); Copy the code and then go to your API Scripts page. Click on New Script, paste the code in the box, then save it. Here's the macro I use for the Saves and Abilities menu: !chatmenu @{selected|character_id} {template:rolls} {{charactername=@{selected|character_name}}} {{header=Saves and Abilities}} {{desc=CHATMENU}} --title:Saving Throws --Fort (@{selected|saving_throws_fortitude}) **@{selected|saving_throws_fortitude_proficiency_display}**,FORT|Ref (@{selected|saving_throws_reflex}) **@{selected|saving_throws_reflex_proficiency_display}**,REF|Will (@{selected|saving_throws_will}) **@{selected|saving_throws_will_proficiency_display}**,WILL --title:Ability Checks --STR @{selected|strength} (@{selected|strength_modifier}),STR|DEX @{selected|dexterity} (@{selected|dexterity_modifier}),DEX|CON @{selected|constitution} (@{selected|constitution_modifier}),CON|INT @{selected|intelligence} (@{selected|intelligence_modifier}),INT|WIS @{selected|wisdom} (@{selected|wisdom_modifier}),WIS|CHA @{selected|charisma} (@{selected|charisma_modifier}),CHA --separator: | : You can set it up as a token action on each PC or as a macro with the Show as Token Action box ticked. It technically works for NPCs too, but I have a separate NPC macro.