← Voltar
Playbook · Ads 360

Do dado da Meta à decisão, em três camadas.

Um caminho simples e repetível: a Meta alimenta a planilha todo dia, o painel mostra e o Claude analisa e executa quando você pede.

1 · Planilha

Matriz padrão de 14 abas. O script grava os dados da Meta às 00:01 e as fórmulas calculam o resto.

2 · Painel

Lê a planilha e mostra filtros, gráficos de crescimento e queda, campanhas e diagnóstico.

3 · Claude

Lê a Meta e a planilha, responde com análise e executa mudanças quando você manda, de forma direta.

Parte 1

Conectar e agendar o preenchimento diário

  1. Baixe o modelo e salve como Planilha Google. Em /modelo, baixe o .xlsx, abra no Google Drive e use Arquivo › Salvar como Planilhas Google. Não renomeie abas nem colunas: o script e o painel leem pelos nomes.
  2. Pegue o ID da conta de anúncios. No Gerenciador de Anúncios, o número da conta aparece no seletor de contas. Você vai usar só os números.
  3. Gere o token da Meta (uma vez). Em business.facebook.com › Configurações do negócio › Usuários › Usuários do sistema, crie um usuário, adicione a conta de anúncios como ativo com acesso de visualização do desempenho e gere um token com a permissão ads_read, sem expiração. Se pedir um app, selecione um do seu negócio (ou crie um em developers.facebook.com).
  4. Instale o script na planilha. Na planilha: Extensões › Apps Script. Apague o código que vem de exemplo, cole o script abaixo e salve. Em Configurações do projeto, defina o fuso horário como America/Sao_Paulo.
  5. Configure a conexão. Recarregue a planilha. Aparece o menu Ads 360. Clique em 1. Configurar conexão com a Meta, autorize as permissões do Google na primeira vez e informe: ID da conta, token e a data inicial do histórico.
  6. Teste com "Atualizar agora". Ads 360 › Atualizar agora. As abas Base_Conta, Base_Campanhas e Base_Diario são preenchidas, e a célula A3 de cada uma mostra o horário da atualização.
  7. Ative o preenchimento diário. Ads 360 › 2. Ativar atualização diária (00:01). A partir daí a planilha se atualiza sozinha todo dia. O Google executa em uma janela de cerca de 15 minutos em torno do horário. Se a Meta recusar o token ou algo falhar, você recebe um e-mail.
  8. Conecte o painel. Compartilhe a planilha como "qualquer pessoa com o link pode ver" (nunca como editor), copie o link e cole na página inicial do Ads 360.
Script de atualizaçãoBaixar .gs
Ver o código
/**
 * ADS 360 · Atualização automática da planilha com dados da Meta
 * -----------------------------------------------------------------
 * Preenche as abas Base_Conta, Base_Campanhas e Base_Diario do modelo Ads 360
 * a partir da API de Marketing da Meta. Só escreve nas colunas de dado (azuis)
 * e replica as fórmulas (pretas) para as linhas novas.
 *
 * Instalação: veja o playbook em /playbook.
 * Segredos (token e conta) ficam em "Propriedades do script", não nas células.
 */

var API = 'https://graph.facebook.com/v21.0/';
var TZ = 'America/Sao_Paulo';
var HEADER_ROW = 4;
var FIRST_ROW = 5;

var IN_CONTA = ['Mês', 'Início', 'Fim', 'Situação', 'Investimento', 'Impressões', 'Alcance', 'Cliques no link', 'Compras', 'ROAS Meta'];
var IN_CAMPS = ['Mês', 'Campanha', 'Objetivo', 'Investimento', 'Impressões', 'Alcance', 'Cliques no link', 'Compras', 'ROAS Meta'];
var IN_DIARIO = ['Data', 'Investimento', 'Impressões', 'Cliques no link', 'Compras', 'ROAS Meta'];

var OBJETIVOS = {
  OUTCOME_SALES: 'Vendas', CONVERSIONS: 'Vendas', PRODUCT_CATALOG_SALES: 'Vendas',
  OUTCOME_TRAFFIC: 'Tráfego', LINK_CLICKS: 'Tráfego',
  OUTCOME_ENGAGEMENT: 'Engajamento', POST_ENGAGEMENT: 'Engajamento', PAGE_LIKES: 'Engajamento', VIDEO_VIEWS: 'Engajamento',
  OUTCOME_AWARENESS: 'Reconhecimento', REACH: 'Reconhecimento', BRAND_AWARENESS: 'Reconhecimento',
  OUTCOME_LEADS: 'Leads', LEAD_GENERATION: 'Leads',
  OUTCOME_APP_PROMOTION: 'App', APP_INSTALLS: 'App', MESSAGES: 'Mensagens'
};

/* ============================ MENU ============================ */

function onOpen() {
  SpreadsheetApp.getUi().createMenu('Ads 360')
    .addItem('Atualizar agora', 'atualizarTudo')
    .addSeparator()
    .addItem('1. Configurar conexão com a Meta', 'configurar')
    .addItem('2. Ativar atualização diária (00:01)', 'ativarDiario')
    .addItem('Desativar atualização diária', 'desativarDiario')
    .addToUi();
}

function configurar() {
  var ui = SpreadsheetApp.getUi();
  var props = PropertiesService.getScriptProperties();

  var a = ui.prompt('Conta de anúncios', 'Cole o ID da conta de anúncios (só números, sem "act_"):', ui.ButtonSet.OK_CANCEL);
  if (a.getSelectedButton() !== ui.Button.OK) return;
  var conta = a.getResponseText().replace(/\D/g, '');
  if (!conta) { ui.alert('ID inválido. Use só os números da conta.'); return; }

  var t = ui.prompt('Token da Meta', 'Cole o token de acesso (permissão ads_read). Ele fica guardado só nas propriedades do script.', ui.ButtonSet.OK_CANCEL);
  if (t.getSelectedButton() !== ui.Button.OK) return;
  var token = t.getResponseText().trim();
  if (token.length < 20) { ui.alert('Token inválido.'); return; }

  var d = ui.prompt('Data inicial', 'A partir de que dia buscar o histórico? Formato AAAA-MM-DD. Deixe em branco para os últimos 4 meses.', ui.ButtonSet.OK_CANCEL);
  if (d.getSelectedButton() !== ui.Button.OK) return;
  var inicio = d.getResponseText().trim();
  if (inicio && !/^\d{4}-\d{2}-\d{2}$/.test(inicio)) { ui.alert('Data inválida. Use AAAA-MM-DD.'); return; }

  props.setProperties({ ACCOUNT_ID: conta, META_TOKEN: token, START_DATE: inicio });
  ui.alert('Conexão salva. Agora use Ads 360 › Atualizar agora para testar.');
}

/* ======================== ATUALIZAÇÃO ======================== */

/** Execução manual (menu). Mostra o resultado na tela. */
function atualizarTudo() {
  var ui = SpreadsheetApp.getUi();
  try {
    var r = executar_();
    ui.alert('Atualizado', r.meses + ' meses, ' + r.camps + ' linhas de campanha e ' + r.dias + ' dias gravados.', ui.ButtonSet.OK);
  } catch (e) {
    ui.alert('Não consegui atualizar', String(e.message || e), ui.ButtonSet.OK);
  }
}

/** Alvo do gatilho diário. Sem tela; avisa o dono por e-mail se falhar. */
function atualizarAgendado() {
  try {
    executar_();
  } catch (e) {
    MailApp.sendEmail(Session.getEffectiveUser().getEmail(), 'Ads 360: a atualização diária falhou', String(e.message || e));
    throw e;
  }
}

function executar_() {
  var lock = LockService.getScriptLock();
  if (!lock.tryLock(30000)) throw new Error('Outra atualização está em andamento. Tente de novo em instantes.');
  try {
    var cfg = lerConfig_();
    var hoje = Utilities.formatDate(new Date(), TZ, 'yyyy-MM-dd');
    var desde = cfg.start || primeiroDiaMesesAtras_(hoje, 3);

    var contaRaw = insights_(cfg, { level: 'account', time_increment: 'monthly' }, desde, hoje);
    var campRaw = insights_(cfg, { level: 'campaign', time_increment: 'monthly' }, desde, hoje);
    var diaRaw = insights_(cfg, { level: 'account', time_increment: '1' }, desde, hoje);
    var objetivos = objetivosPorCampanha_(cfg);

    var contaRows = montarConta_(contaRaw);
    var campRows = montarCampanhas_(campRaw, objetivos);
    var diaRows = montarDiario_(diaRaw);

    var ss = SpreadsheetApp.getActiveSpreadsheet();
    var stamp = 'Atualizado automaticamente em ' + Utilities.formatDate(new Date(), TZ, 'dd/MM/yyyy HH:mm');
    gravar_(ss, 'Base_Conta', IN_CONTA, contaRows, stamp);
    gravar_(ss, 'Base_Campanhas', IN_CAMPS, campRows, stamp);
    gravar_(ss, 'Base_Diario', IN_DIARIO, diaRows, stamp);
    SpreadsheetApp.flush();
    return { meses: contaRows.length, camps: campRows.length, dias: diaRows.length };
  } finally {
    lock.releaseLock();
  }
}

function ativarDiario() {
  var ui = SpreadsheetApp.getUi();
  desativar_();
  ScriptApp.newTrigger('atualizarAgendado').timeBased().everyDays(1).atHour(0).nearMinute(1).inTimezone(TZ).create();
  ui.alert('Atualização diária ativada',
    'A planilha será atualizada todos os dias por volta de 00:01 (horário de Brasília). O Google executa dentro de uma janela de cerca de 15 minutos em torno desse horário.',
    ui.ButtonSet.OK);
}

function desativarDiario() {
  var n = desativar_();
  SpreadsheetApp.getUi().alert(n ? 'Atualização diária desativada.' : 'Não havia atualização diária ativa.');
}

function desativar_() {
  var n = 0;
  ScriptApp.getProjectTriggers().forEach(function (t) {
    if (t.getHandlerFunction() === 'atualizarAgendado') { ScriptApp.deleteTrigger(t); n++; }
  });
  return n;
}

/* ===================== TRANSFORMAÇÕES (puras) ===================== */

function num_(v) {
  var n = parseFloat(v);
  return isNaN(n) ? 0 : n;
}

/** Valor do evento de compra em actions ou purchase_roas. */
function valorCompra_(arr) {
  if (!arr) return 0;
  var tipos = ['omni_purchase', 'purchase'];
  for (var t = 0; t < tipos.length; t++) {
    for (var i = 0; i < arr.length; i++) {
      if (arr[i].action_type === tipos[t]) return num_(arr[i].value);
    }
  }
  return 0;
}

function lerInsight_(r) {
  return {
    inv: num_(r.spend),
    imp: num_(r.impressions),
    alc: num_(r.reach),
    cli: num_(r.inline_link_clicks),
    compras: valorCompra_(r.actions),
    roas: valorCompra_(r.purchase_roas)
  };
}

function dt_(s) {
  return new Date(parseInt(s.slice(0, 4), 10), parseInt(s.slice(5, 7), 10) - 1, parseInt(s.slice(8, 10), 10));
}

function primeiroDiaMesesAtras_(hojeStr, meses) {
  var d = dt_(hojeStr);
  var x = new Date(d.getFullYear(), d.getMonth() - meses, 1);
  return x.getFullYear() + '-' + ('0' + (x.getMonth() + 1)).slice(-2) + '-01';
}

function montarConta_(raw) {
  return raw.map(function (r) {
    var m = lerInsight_(r);
    var ini = dt_(r.date_start), fim = dt_(r.date_stop);
    var fimDoMes = new Date(ini.getFullYear(), ini.getMonth() + 1, 0);
    return {
      'Mês': new Date(ini.getFullYear(), ini.getMonth(), 1), 'Início': ini, 'Fim': fim,
      'Situação': fim.getTime() < fimDoMes.getTime() ? 'Parcial' : 'Fechado',
      'Investimento': m.inv, 'Impressões': m.imp, 'Alcance': m.alc, 'Cliques no link': m.cli,
      'Compras': m.compras, 'ROAS Meta': m.roas
    };
  }).filter(function (l) { return l['Investimento'] > 0; })
    .sort(function (a, b) { return a['Mês'] - b['Mês']; });
}

function montarCampanhas_(raw, objetivos) {
  return raw.map(function (r) {
    var m = lerInsight_(r);
    var ini = dt_(r.date_start);
    var bruto = objetivos[r.campaign_id];
    return {
      'Mês': new Date(ini.getFullYear(), ini.getMonth(), 1), 'Campanha': r.campaign_name || '',
      'Objetivo': OBJETIVOS[bruto] || bruto || 'Outros',
      'Investimento': m.inv, 'Impressões': m.imp, 'Alcance': m.alc, 'Cliques no link': m.cli,
      'Compras': m.compras, 'ROAS Meta': m.roas
    };
  }).filter(function (l) { return l['Investimento'] > 0; })
    .sort(function (a, b) { return (a['Mês'] - b['Mês']) || (b['Investimento'] - a['Investimento']); });
}

function montarDiario_(raw) {
  return raw.map(function (r) {
    var m = lerInsight_(r);
    return {
      'Data': dt_(r.date_start), 'Investimento': m.inv, 'Impressões': m.imp, 'Cliques no link': m.cli,
      'Compras': m.compras, 'ROAS Meta': m.roas
    };
  }).filter(function (l) { return l['Investimento'] > 0 || l['Compras'] > 0; })
    .sort(function (a, b) { return a['Data'] - b['Data']; });
}

/* ========================= API DA META ========================= */

function lerConfig_() {
  var p = PropertiesService.getScriptProperties().getProperties();
  if (!p.ACCOUNT_ID || !p.META_TOKEN) throw new Error('Conexão não configurada. Use Ads 360 › 1. Configurar conexão com a Meta.');
  return { account: p.ACCOUNT_ID, token: p.META_TOKEN, start: p.START_DATE || '' };
}

function qs_(o) {
  return Object.keys(o).map(function (k) { return encodeURIComponent(k) + '=' + encodeURIComponent(o[k]); }).join('&');
}

function get_(url, token) {
  for (var tentativa = 0; tentativa < 4; tentativa++) {
    var res = UrlFetchApp.fetch(url, { headers: { Authorization: 'Bearer ' + token }, muteHttpExceptions: true });
    var j;
    try { j = JSON.parse(res.getContentText()); } catch (e) { throw new Error('Resposta inesperada da Meta (HTTP ' + res.getResponseCode() + ').'); }
    if (!j.error) return j;
    var limite = [4, 17, 32, 613].indexOf(j.error.code) >= 0;
    if (limite && tentativa < 3) { Utilities.sleep(20000 * (tentativa + 1)); continue; }
    if (j.error.code === 190) throw new Error('O token da Meta expirou ou é inválido. Gere um novo e use Configurar conexão.');
    throw new Error('Meta: ' + j.error.message);
  }
}

function paginar_(url, token) {
  var out = [];
  while (url) {
    var j = get_(url, token);
    out = out.concat(j.data || []);
    url = j.paging && j.paging.next ? j.paging.next : null;
  }
  return out;
}

function insights_(cfg, extra, desde, ate) {
  var p = {
    fields: 'spend,impressions,reach,inline_link_clicks,actions,purchase_roas' + (extra.level === 'campaign' ? ',campaign_id,campaign_name' : ''),
    time_range: JSON.stringify({ since: desde, until: ate }),
    limit: 500
  };
  Object.keys(extra).forEach(function (k) { p[k] = extra[k]; });
  return paginar_(API + 'act_' + cfg.account + '/insights?' + qs_(p), cfg.token);
}

function objetivosPorCampanha_(cfg) {
  var lista = paginar_(API + 'act_' + cfg.account + '/campaigns?' + qs_({ fields: 'id,objective', limit: 500 }), cfg.token);
  var mapa = {};
  lista.forEach(function (c) { mapa[c.id] = c.objective; });
  return mapa;
}

/* ======================== GRAVAÇÃO NA ABA ======================== */

function gravar_(ss, nome, entradas, linhas, stamp) {
  var sh = ss.getSheetByName(nome);
  if (!sh) throw new Error('A aba "' + nome + '" não existe. Use o modelo padrão do Ads 360.');
  var lastCol = sh.getLastColumn();
  var headers = sh.getRange(HEADER_ROW, 1, 1, lastCol).getValues()[0].map(function (h) { return String(h).trim(); });
  var idx = {};
  headers.forEach(function (h, i) { if (h) idx[h] = i + 1; });
  entradas.forEach(function (h) {
    if (!idx[h]) throw new Error('A aba "' + nome + '" não tem a coluna "' + h + '". Não renomeie as colunas do modelo.');
  });

  var n = linhas.length;
  var lastRow = Math.max(sh.getLastRow(), FIRST_ROW);
  var antigas = lastRow - FIRST_ROW + 1;
  var formulaCols = [];
  var formulas = sh.getRange(FIRST_ROW, 1, 1, lastCol).getFormulas()[0];
  headers.forEach(function (h, i) {
    if (h && entradas.indexOf(h) < 0 && formulas[i]) formulaCols.push(i + 1);
  });

  // limpa os dados antigos das colunas de entrada (mantém formatação)
  entradas.forEach(function (h) { sh.getRange(FIRST_ROW, idx[h], antigas, 1).clearContent(); });
  if (n === 0) { sh.getRange(3, 1).setValue(stamp + ' · sem dados no período'); return; }

  // formata e grava as colunas de entrada
  entradas.forEach(function (h) {
    var col = idx[h];
    if (n > 1) sh.getRange(FIRST_ROW, col).copyTo(sh.getRange(FIRST_ROW + 1, col, n - 1, 1), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
    var vals = linhas.map(function (l) { return [l[h] === undefined ? '' : l[h]]; });
    sh.getRange(FIRST_ROW, col, n, 1).setValues(vals);
  });

  // replica as fórmulas e apaga as sobras
  formulaCols.forEach(function (c) {
    if (n > 1) sh.getRange(FIRST_ROW, c).copyTo(sh.getRange(FIRST_ROW + 1, c, n - 1, 1));
    var sobra = lastRow - (FIRST_ROW + n - 1);
    if (sobra > 0) sh.getRange(FIRST_ROW + n, c, sobra, 1).clearContent();
  });
  sh.getRange(3, 1).setValue(stamp);
}
Dois botões de atualizar, cada um com uma função

Ads 360 › Atualizar agora (menu da planilha) busca os dados na Meta e grava na planilha. ↻ Atualizar agora (painel) relê a planilha. Para ver o que acabou de acontecer na conta, rode primeiro o da planilha e depois o do painel. O painel nunca é mais novo do que a planilha.

O modelo comporta 36 meses na conta e 1.000 linhas em campanhas e dias. O token fica nas propriedades do script, não em células. Quem tem acesso de editor à planilha consegue abrir o script, então compartilhe como editor só com quem é de confiança.

Parte 2

Comandos diretos para o Claude

Troque o que está entre colchetes e envie. O Claude lê a Meta e a planilha para responder. Quem grava na planilha todo dia é o script, não o Claude.

Resumo semanal para o CEO · Toda segunda-feira

Traga os dados da semana passada (segunda a domingo) da conta [NOME DA CONTA]: investimento, receita atribuída, ROAS, compras e CPA. Compare com a semana anterior e destaque o que mais mudou.

Diagnóstico do mês · Todo dia 1º

Leia a planilha [LINK] e faça o diagnóstico do último mês fechado: o que cresceu, o que caiu e as 3 decisões mais importantes, em ordem de impacto.

Quais campanhas escalar · Antes de mexer em orçamento

Quais campanhas posso escalar? Use pelo menos 14 dias de dados e mínimo de 20 compras. Confira dia a dia se o resultado não depende de 1 ou 2 dias. Mostre ROAS, compras e frequência de cada uma.

O que cortar · Semanal

Liste as campanhas com ROAS abaixo do break-even de [X] e mais de R$ [Y] investidos nos últimos 14 dias. Diga quanto de verba está presa nelas.

Mudar orçamento · Depois de decidir

Aumente em [20]% o orçamento diário da campanha [NOME]. Mostre o valor atual e o novo antes de aplicar e, depois, confirme que a mudança está publicada.

Dependência de picos · Mensal

Em [MÊS], quanto da receita veio dos 2 melhores dias? Qual foi o ROAS sem eles? Existe risco de eu estar lendo um pico como tendência?

Conferir a planilha · Quando algo parecer estranho

Compare a planilha [LINK] com os dados da Meta dos últimos 30 dias e me diga se algum mês ou campanha está desatualizado ou diferente.

Alerta diário · Agendar uma vez

Todo dia às 08:00 confira as campanhas de vendas e me avise se alguma passar de frequência 3,5 ou ficar abaixo do break-even por 3 dias seguidos.

Parte 3

Regras para não errar a decisão

  1. Diga sempre quatro coisas: período, conta, critério e ação. Exemplo: "últimos 14 dias, conta X, ROAS acima de 5 com 20 compras, liste".
  2. Nunca escale com menos de 7 dias de dados. Um dia de pico pode enganar a média de uma semana inteira.
  3. Peça sempre o ROAS sem os 2 melhores dias antes de aumentar verba.
  4. Receita atribuída é o que a Meta diz. Confirme com o faturamento da loja antes de decisões grandes.
  5. Antes de qualquer mudança em campanha, peça o valor atual e o novo. Depois, peça a confirmação de que foi publicada.
  6. A margem real (aba Premissas) define o break-even. Enquanto for provisória, trate os cortes como hipótese.