State Machine para Automações: O Padrão Que Impede Seus Scripts de Virarem Espaguete (Python Puro)
Você já teve aquele script de automação que funcionava perfeitamente até o servidor reiniciar no meio da execução? E quando voltou, ninguém sabia em que ponto tinha parado — se tinha enviado o email, se tinha gravado no banco, se tinha travado no meio do webhook?
State machine não é frescura de arquiteto. É a diferença entre uma automação que se recupera sozinha e uma que precisa de você às 3 da manhã resolvendo estado inconsistente com script ad-hoc que você escreveu dormindo.
Hoze eu vou te mostrar como construir uma máquina de estados para automações em Python puro — sem bibliotecas pesadas, sem framework, sem complicação. Só código que funciona, persiste estado em arquivo local e sobrevive a quedas de energia.

O Problema Que Todo Mundo Ignora (Até Que Explode)
A maioria das automações que eu vejo por aí segue o mesmo padrão:
# O clássico script linear que todo mundo escreve
def executar_automacao():
passo_1() # baixa dados da API
passo_2() # processa os dados
passo_3() # salva no banco
passo_4() # envia notificação
passo_5() # gera relatório
Parece inofensivo, né? Até que o passo 3 falha.
O que acontece?
- Os dados foram baixados (passo 1 ✅)
- Foram processados (passo 2 ✅)
- Mas não foram salvos (passo 3 ❌)
- E o passo 4 nem executou
Agora você reexecuta o script. Ele baixa os dados de novo, processa de novo, e talvez — dependendo do timing — duplique registros ou gere relatórios inconsistentes. Ou pior: o passo 4 envia uma notificação que já foi enviada antes porque você não tinha como saber.
Isso não é bug. É falta de controle de estado. E state machine resolve isso de uma vez.
O Que É Uma State Machine (Sem Bla-Bla-Bla Acadêmico)
Uma máquina de estados é, na prática, um conceito simples:
Sua automação sempre está em UM estado definido. Ela só muda de estado quando uma transição explícita acontece. E você sabe exatamente em que ponto parou.
Pensando nos estados da nossa automação de exemplo:
IDLE→ esperando execuçãoFETCHING→ baixando dadosPROCESSING→ transformando dadosPERSISTING→ salvando no bancoNOTIFYING→ enviando alertasREPORTING→ gerando relatórioCOMPLETED→ terminou com sucessoFAILED→ erro em algum pontoRECOVERING→ tentando se recuperar
Cada estado tem transições válidas. Do IDLE você vai para FETCHING. Do FETCHING, só pode ir para PROCESSING (sucesso) ou FAILED (erro). Não tem como pular direto para COMPLETED sem passar por tudo antes.
Implementação Prática: State Machine em Python Puro
Vamos construir algo que funciona de verdade. Primeiro, a base:
from enum import Enum, auto
from typing import Callable, Dict, Set
from datetime import datetime
import json
import os
class Estado(Enum):
IDLE = auto()
FETCHING = auto()
PROCESSING = auto()
PERSISTING = auto()
NOTIFYING = auto()
REPORTING = auto()
COMPLETED = auto()
FAILED = auto()
RECOVERING = auto()
class TransicaoInvalida(Exception):
"""Tentativa de transição que não está definida"""
pass
class StateMachine:
def __init__(self, estado_inicial: Estado, nome: str = "automacao"):
self.nome = nome
self.estado_atual = estado_inicial
self.historico: list[dict] = []
self._transicoes: Dict[Estado, Set[Estado]] = {}
self._arquivo_estado = f"/tmp/{nome}_estado.json"
self._definir_transicoes_padrao()
def _definir_transicoes_padrao(self):
"""Define quais transições são válidas para cada estado"""
self._transicoes = {
Estado.IDLE: {Estado.FETCHING, Estado.FAILED},
Estado.FETCHING: {Estado.PROCESSING, Estado.FAILED},
Estado.PROCESSING: {Estado.PERSISTING, Estado.FAILED},
Estado.PERSISTING: {Estado.NOTIFYING, Estado.FAILED},
Estado.NOTIFYING: {Estado.REPORTING, Estado.FAILED},
Estado.REPORTING: {Estado.COMPLETED, Estado.FAILED},
Estado.FAILED: {Estado.RECOVERING, Estado.IDLE},
Estado.RECOVERING: {Estado.FETCHING, Estado.PROCESSING, Estado.PERSISTING},
Estado.COMPLETED: set(), # estado terminal
}
def pode_transicionar(self, destino: Estado) -> bool:
"""Verifica se a transição é válida sem executar"""
permitidas = self._transicoes.get(self.estado_atual, set())
return destino in permitidas
def transicionar(self, destino: Estado, contexto: str = ""):
"""Executa a transição de estado com validação"""
if not self.pode_transicionar(destino):
raise TransicaoInvalida(
f"[{self.nome}] Transição inválida: "
f"{self.estado_atual.name} -> {destino.name}. "
f"Transições permitidas: {[e.name for e in self._transicoes.get(self.estado_atual, set())]}"
)
anterior = self.estado_atual
self.estado_atual = destino
self._registrar_historico(anterior, destino, contexto)
self._persistir_estado()
print(f"[{self.nome}] {anterior.name} -> {destino.name} {contexto}")
def _registrar_historico(self, de: Estado, para: Estado, contexto: str):
self.historico.append({
"de": de.name,
"para": para.name,
"contexto": contexto,
"timestamp": datetime.now().isoformat(),
})
def _persistir_estado(self):
"""Salva o estado atual em arquivo local para recuperação"""
snapshot = {
"nome": self.nome,
"estado_atual": self.estado_atual.name,
"historico": self.historico,
"ultima_atualizacao": datetime.now().isoformat(),
}
with open(self._arquivo_estado, "w") as f:
json.dump(snapshot, f, indent=2)
@classmethod
def restaurar(cls, nome: str = "automacao"):
"""Restaura estado de arquivo persistente"""
arquivo = f"/tmp/{nome}_estado.json"
if not os.path.exists(arquivo):
sm = cls(Estado.IDLE, nome)
return sm
with open(arquivo) as f:
dados = json.load(f)
estado = Estado[dados["estado_atual"]]
sm = cls(estado, nome)
sm.historico = dados.get("historico", [])
return sm
Isso aqui é a base. Valida transições, registra histórico e persiste estado em disco. Se o processo morrer no meio, você restaura exatamente de onde parou.

Orquestrando a Automação Real
Agora vamos usar essa máquina de estados para orquestrar nossa automação de exemplo:
class AutomacaoPipeline:
def __init__(self, nome: str = "pipeline_dados"):
self.sm = StateMachine(Estado.IDLE, nome)
self.dados_baixados = None
self.dados_processados = None
def executar(self):
"""Executa o pipeline completo com controle de estado"""
try:
# Transição: IDLE -> FETCHING
self.sm.transicionar(Estado.FETCHING, "iniciando pipeline")
self._baixar_dados()
# Transição: FETCHING -> PROCESSING
self.sm.transicionar(Estado.PROCESSING, "dados recebidos")
self._processar_dados()
# Transição: PROCESSING -> PERSISTING
self.sm.transicionar(Estado.PERSISTING, "dados transformados")
self._persistir_dados()
# Transição: PERSISTING -> NOTIFYING
self.sm.transicionar(Estado.NOTIFYING, "dados salvos")
self._enviar_notificacao()
# Transição: NOTIFYING -> REPORTING
self.sm.transicionar(Estado.REPORTING, "notificação enviada")
self._gerar_relatorio()
# Transição: REPORTING -> COMPLETED
self.sm.transicionar(Estado.COMPLETED, "pipeline finalizado")
except Exception as e:
self._tratar_erro(e)
def _baixar_dados(self):
# Simula fetch de API externa
import time
time.sleep(0.1) # aqui seria requests.get()
self.dados_baixados = {"registros": 1500}
def _processar_dados(self):
# Simula transformação
self.dados_processados = {
"validos": int(self.dados_baixados["registros"] * 0.95),
"invalidos": int(self.dados_baixados["registros"] * 0.05),
}
def _persistir_dados(self):
# Simula write no banco
pass
def _enviar_notificacao(self):
# Simula envio de alerta
pass
def _gerar_relatorio(self):
# Simula geração de relatório
pass
def _tratar_erro(self, erro: Exception):
"""Registra o erro e tenta recuperar"""
estado_antes_erro = self.sm.estado_atual
self.sm.transicionar(Estado.FAILED, f"erro: {erro}")
print(f" [recuperação] tentando retomar de {estado_antes_erro.name}")
# Dependendo do estado, recupera de um ponto diferente
if estado_antes_erro in (Estado.FETCHING, Estado.PROCESSING):
self.sm.transicionar(Estado.RECOVERING, "fallback para fetching")
self.sm.transicionar(Estado.FETCHING, "retry após recuperação")
self._baixar_dados()
self.sm.transicionar(Estado.PROCESSING, "retry bem-sucedido")
# ... continua a partir daqui
else:
print(" [erro] estado não recuperável automaticamente")
Note o que acontece aqui: cada passo é uma transição explícita. Se qualquer coisa falha, o estado vai para FAILED e o sistema sabe exatamente onde parou. Não tem adivinhação. Não tem “será que o passo 3 rodou?”.
O Box do Perrengue Real
🔧 O Perrengue: Eu implementei isso em produção depois de um deploy que matou o container no meio de um sync de 40 mil registros. O script anterior — sem state machine — ficou 3 dias processando duplicata porque ninguém sabia quantos registros já tinham sido inseridos. Com state machine, o container voltou, leu o arquivo de estado, descobriu que estava em
PERSISTINGcom 23.847 registros salvos, e continuou do registro 23.848. Zero duplicata. Zero intervenção manual.
Por Que Isso É Melhor Que if/else?
Você pode estar pensando: “isso tudo é overkill, eu uso if e try/except“. Eu também pensava assim. Até o dia em que:
- Um
exceptgenérico engoliu um erro de rede e o script continuou processando dados vazios - Uma variável de estado ficou como
Truemas o processo já tinha morrido - Dois scripts rodaram ao mesmo tempo e ambos acharam que eram o único ativo
State machine resolve esses problemas porque:
- Transições são explícitas — não tem como pular etapa
- Estado é persistido — sobrevive a crashes e reinícios
- Histórico é auditável — você sabe exatamente o que aconteceu e quando
- Validação é automática — tentativa de transição inválida gera exceção imediata

Adicionando Timeouts e Guard Clauses
Uma state machine de verdade precisa de timeouts. Se o estado FETCHING durar mais de 30 segundos, algo está errado:
import time
class StateMachineComTimeout(StateMachine):
def __init__(self, estado_inicial: Estado, nome: str = "automacao",
timeout_segundos: int = 30):
super().__init__(estado_inicial, nome)
self.timeout = timeout_segundos
self._inicio_estado = None
def transicionar(self, destino: Estado, contexto: str = ""):
# Verifica timeout do estado anterior
if self._inicio_estado is not None:
decorrido = time.time() - self._inicio_estado
if decorrido > self.timeout:
print(f" [timeout] estado {self.estado_atual.name} "
f"durou {decorrido:.1f}s (limite: {self.timeout}s)")
super().transicionar(destino, contexto)
self._inicio_estado = time.time()
Simples. Efetivo. E evita aquela situação onde seu script fica travado em PROCESSING por 4 horas porque a API externa entrou em modo zombie.
Integrando com Systemd e Logs
Para rodar em produção, você não quer só persistir em arquivo — quer logs estruturados:
import logging
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(name)s] %(message)s",
handlers=[
logging.FileHandler("/var/log/automente/state_machine.log"),
logging.StreamHandler(),
]
)
logger = logging.getLogger("state_machine")
# No StateMachine, substitua o print por:
# logger.info(f"[{self.nome}] {anterior.name} -> {destino.name} {contexto}")
Com isso, cada transição gera um log com timestamp. Você pode grep, monitorar com fail2ban, enviar para um dashboard — o céu é o limite.
Quando NÃO Usar State Machine
Vamos ser honestos: state machine não é bala de prata. Não use quando:
- O fluxo tem 2 passos e nunca vai crescer — um
try/exceptresolve - Você precisa de paralelismo real — state machine é linear por natureza (embora você possa ter múltiplas instâncias)
- O estado depende de condições externas complexas — talvez um event-driven architecture faça mais sentido
Use quando o custo de falhar no meio do processo é maior que o custo de escrever o código extra. Na minha experiência, esse ponto é atingido na automação #3 ou #4.
Conclusão: Controle é Liberdade
State machine para automações não é sobre ser fancy. É sobre dormir tranquilo sabendo que suas automações sabem onde estão, para onde vão, e o que fazer se algo der errado.
O código completo está acima — copie, adapte, use. Sem dependências externas, sem framework, sem complicação. Só Python puro resolvendo um problema real.
Qual automação sua precisa desse controle? Me conta nos comentários — ou melhor, me diz aqui embaixo: qual pipeline de dados, qual sync, qual processo batch você quer ver implementado com state machine no próximo artigo?
Leitura relacionada:
- Circuit Breaker Local com Python e SQLite — outro padrão de resiliência
- Container de Injeção de Dependência em Python Puro — arquitetura modular
- Dead Man’s Switch Local — detecção de silêncio em automações
