Você precisa importar 3 mil produtos de uma planilha do fornecedor pra dentro do Protheus. Ou lançar 500 pedidos de venda vindos de um marketplace. Ou rodar baixa de mil títulos de uma vez. Abrir a interface e digitar manualmente está fora de cogitação — e RecLock direto na tabela é tiro no pé porque você pula gatilhos, validações, integrações fiscais, contábeis, MVs.

A resposta certa nesse cenário é MSExecAuto: a forma oficial de executar uma rotina padrão (MATA010, FINA070, MATA410…) como se um usuário tivesse digitado tudo na tela, mas sem abrir tela nenhuma. O Protheus roda toda a regra de negócio no fundo, valida, grava, contabiliza — e você só passa um array de campos.

Como funciona por baixo

Toda rotina padrão do Protheus tem uma função principal (MATA010, MATA030, FINA070, etc.) que sabe se comportar de dois jeitos: interativo (chamado pelo menu, abre tela) ou automático (chamado por MSExecAuto, lê os dados de variáveis e arrays). A função detecta o modo pelo contexto e pula a parte de tela quando estiver em ExecAuto.

Sintaxe básica:

MSExecAuto({|x, y| MATA010(x, y)}, aDados, nOpcao)

O primeiro argumento é um code block que recebe os parâmetros e chama a rotina alvo. aDados é um array com os campos a preencher. nOpcao indica a operação:

  • 3 — inclusão
  • 4 — alteração
  • 5 — exclusão

As variáveis PRIVATE obrigatórias

Antes de qualquer chamada, você precisa declarar variáveis em escopo PRIVATE (não LOCAL) que o framework lê do contexto:

PRIVATE lMsErroAuto := .F.   // vira .T. se rotina falhar
PRIVATE lMsHelpAuto := .T.   // suprime Help interativo (essencial em batch)

Isso não é opcional. Se você declarar LOCAL lMsErroAuto, a rotina interna não enxerga, vai escrever em outro escopo, e quando você testar depois sempre vai estar .F. mesmo que tenha falhado.

Exemplo simples: cadastrar um produto (MATA010)

User Function ATImpProd()
    Local aArea := GetArea()
    Local aProduto := {}

    PRIVATE lMsErroAuto := .F.
    PRIVATE lMsHelpAuto := .T.

    aProduto := { ;
        {"B1_COD",    "PROD001",    NIL}, ;
        {"B1_DESC",   "Caneta esferográfica azul", NIL}, ;
        {"B1_TIPO",   "MC",         NIL}, ;
        {"B1_UM",     "UN",         NIL}, ;
        {"B1_LOCPAD", "01",         NIL}, ;
        {"B1_GRUPO",  "0001",       NIL}, ;
        {"B1_PRV1",   3.50,         NIL}  ;
    }

    MSExecAuto({|x, y| MATA010(x, y)}, aProduto, 3)

    If lMsErroAuto
        ConOut("Erro ao incluir produto:")
        ConOut(MostraErro())  // ou GetAutoGrLog() em batch puro
    Else
        ConOut("Produto incluído: " + SB1->B1_COD)
    EndIf

    RestArea(aArea)
Return

Cada linha do array tem 3 elementos: nome do campo, valor, e NIL (reservado pra modo M>F, raramente usado). Não precisa preencher todos os campos da SX3 — só os obrigatórios e os que você quer setar; o resto pega default.

Exemplo composto: pedido de venda (MATA410)

Rotinas com cabeçalho + itens recebem dois (ou mais) arrays separados. O bloco passa cada um como argumento:

User Function ATImpPedido()
    Local aArea := GetArea()

    PRIVATE lMsErroAuto := .F.
    PRIVATE lMsHelpAuto := .T.

    Local aCabec := { ;
        {"C5_TIPO",    "N",      NIL}, ;
        {"C5_CLIENTE", "000001", NIL}, ;
        {"C5_LOJACLI", "01",     NIL}, ;
        {"C5_CONDPAG", "001",    NIL}, ;
        {"C5_EMISSAO", Date(),   NIL}  ;
    }

    Local aItens := { ;
        { ;
            {"C6_ITEM",    "01",      NIL}, ;
            {"C6_PRODUTO", "PROD001", NIL}, ;
            {"C6_QTDVEN",  10,        NIL}, ;
            {"C6_PRCVEN",  25.00,     NIL}, ;
            {"C6_TES",     "501",     NIL}  ;
        } ;
    }

    MSExecAuto({|x, y, z| MATA410(x, y, z)}, aCabec, aItens, 3)

    If lMsErroAuto
        ConOut(GetAutoGrLog())
    EndIf

    RestArea(aArea)
Return

O array de itens é um array de arrays — cada item do pedido é um array no mesmo formato do cabeçalho. Quer 5 itens no pedido? Cinco sub-arrays.

Tratamento de erro: MostraErro vs GetAutoGrLog

Quando lMsErroAuto = .T., você tem duas formas de pegar o motivo:

  • MostraErro() — abre janela com erro (use em rotina interativa).
  • GetAutoGrLog() — retorna string com log completo (use em batch, jobs, web service).

Em job sem interface, MostraErro trava. Sempre GetAutoGrLog() em batch. Padrão comum pra logar:

If lMsErroAuto
    cLog := GetAutoGrLog()
    ConOut("=== ERRO ExecAuto ===")
    ConOut(cLog)
    // grave em arquivo, banco, ou retorne pra integração
EndIf

Transação: rollback automático

Se a rotina falha no meio (cabeçalho gravado, item dá erro), você quer reverter tudo. Embrulhe num Begin Transaction:

Begin Transaction
    MSExecAuto({|x, y, z| MATA410(x, y, z)}, aCabec, aItens, 3)

    If lMsErroAuto
        DisarmTransaction()
        Break
    EndIf
End Transaction

DisarmTransaction() + Break faz rollback de tudo que foi gravado dentro do bloco. Sem isso, em alguns cenários o cabeçalho fica órfão.

Pegadinhas que travam projeto

1. PRIVATE não LOCAL

Já mencionado, mas vale repetir: variáveis de controle do ExecAuto precisam ser visíveis em todos os escopos chamados. LOCAL não funciona. Em rotinas customizadas (User Functions), declare PRIVATE no topo da função.

2. cFilAnt e usuário

O ExecAuto roda no contexto da filial corrente (cFilAnt) e usuário corrente. Se sua rotina batch precisa rodar em filial diferente, faça cFilAnt := "02" antes (e restaure depois). Pra job sem usuário logado, prepare ambiente com RpcSetType(3) + RpcSetEnv().

3. MV_PAR1, MV_PAR2…

Algumas rotinas (especialmente processos como FINA070 — baixa) usam MV_PAR1, MV_PAR2 etc. pra ler parâmetros da pergunta padrão (SX1). Em ExecAuto você precisa setar essas variáveis manualmente antes da chamada:

PRIVATE MV_PAR01 := "001"
PRIVATE MV_PAR02 := "999"
// ...e assim por diante

4. Tabela bloqueada por outro processo

Se outra rotina interativa está com a SB1 em RecLock, seu ExecAuto vai falhar com erro de lock. Em integrações de alta concorrência, vale fazer retry com pequeno delay quando o erro é por lock.

5. Versões diferentes de rotinas

Algumas rotinas têm versão "N" (nova): MATA410 vs MATA410N, MATA103 vs MATA103N. A nova geralmente exige campos diferentes ou ordem diferente nos parâmetros. Sempre olhe a documentação da sua versão do Protheus, não copie de blog antigo cego.

Quando NÃO usar ExecAuto

flowchart TD Q1{Quantos registros?} -->|1-100 com regras complexas| EA[ExecAuto
respeita gatilhos+validacoes] Q1 -->|100-10mil em batch| Q2{Tem regra fiscal/contabil?} Q2 -->|Sim| EA2[ExecAuto
mesmo lento] Q2 -->|Nao, dado puro| ETL[ETL: SQL bulk insert
+ recalculo via job] Q1 -->|10mil+ ou import inicial| ETL2[ETL: bulk + reindex
ou DBAccess Tools] EA --> OK[Confiavel] EA2 --> OK ETL --> RISK[Pula validacoes:
assume responsabilidade] ETL2 --> RISK

ExecAuto é a opção certa pra centenas a alguns milhares de registros. Acima disso, o overhead de validações torna o processo lento (cada registro pode levar segundos). Pra carga inicial de massa (50k+ produtos numa migração), o caminho normalmente é: SQL bulk → recálculo de saldos via job batch → reconciliação.

Tabela das rotinas mais usadas

FunçãoMóduloO que fazEstrutura aDados
MATA010EstoqueCadastro de produto (SB1)1 array de campos
MATA020ComprasCadastro de fornecedor (SA2)1 array
MATA030FaturamentoCadastro de cliente (SA1)1 array
MATA103ComprasNF de entradacabec + itens
MATA410FaturamentoPedido de vendacabec + itens
MATA460FaturamentoFaturamento de pedidocabec + itens
FINA040FinanceiroInclusão de título (SE1/SE2)1 array
FINA070FinanceiroBaixa de títulocabec + array de baixa
FINA340FinanceiroCompensação automática1 array
MATA080EstoqueMovimento interno (SD3)1 array

Boilerplate que vale colar no projeto

Em integrações com vários ExecAutos, vale ter uma função wrapper que padroniza tratamento de erro e log:

User Function ATExecAuto(bRotina, aDados, nOpc, cContexto)
    Local lOk    := .T.
    Local cErro  := ""

    PRIVATE lMsErroAuto := .F.
    PRIVATE lMsHelpAuto := .T.

    Begin Transaction
        Eval(bRotina, aDados, nOpc)

        If lMsErroAuto
            cErro := GetAutoGrLog()
            DisarmTransaction()
            lOk := .F.
            Break
        EndIf
    End Transaction

    If !lOk
        ConOut("[" + cContexto + "] FALHOU: " + Left(cErro, 200))
        // grava em log estruturado, manda alerta, etc.
    EndIf

Return {lOk, cErro}

Uso:

aRet := U_ATExecAuto({|x, y| MATA010(x, y)}, aProduto, 3, "IMP-PRODUTO-" + cCodFornec)

Vale o overhead

Tem dev que prefere RecLock direto pra ganhar 10x de performance. Funciona — até o dia que o gatilho da SB1 que recalcula custo médio não roda, ou a integração fiscal não dispara, ou o módulo de estoque entra em desacordo com a contabilidade. ExecAuto existe pra isso não acontecer. Você abre mão de velocidade pra ganhar consistência com todo o resto do ERP.

Pra os casos onde performance é crítica e você sabe exatamente o que está pulando (carga inicial, importação histórica), aí sim vai de SQL ou ETL. Pra integração corrente do dia a dia, ExecAuto é o caminho. Doc oficial dispersa, mas o fundamento está no TDN do MSExecAuto e nas páginas de cada rotina (MATA010, FINA070, etc.).