Problem with pack.addSyncTable: The sync table does not populate with data

Hello everyone,

I’m trying to use pack.addSyncTable in a Coda Pack to create a sync table that fetches data from an external API. The idea is for the table to automatically update with data retrieved from the API.

However, when I try to sync the table in my Coda document, it does not populate with any data. It simply remains empty with the message: "No rows were returned during sync.

Here is the code I’m using:
import * as coda from “@codahq/packs-sdk”;
export const pack = coda.newPack();

pack.addNetworkDomain(“jsonplaceholder.typicode.com”);

pack.addSyncTable({
name: “Posts”,
description: “Syncs a list of posts from JSONPlaceholder.”,
identityName: “Post”,
schema: coda.makeObjectSchema({
properties: {
id: { type: coda.ValueType.Number, description: “Post ID” },
title: { type: coda.ValueType.String, description: “Post Title” },
body: { type: coda.ValueType.String, description: “Post Content” },
},
displayProperty: “title”,
idProperty: “id”,
}),
formula: {
name: “SyncPosts”,
description: “Fetches posts from JSONPlaceholder.”,
parameters: [
coda.makeParameter({
type: coda.ParameterType.Number,
name: “limit”,
description: “Number of posts to fetch. Default is 5.”,
optional: true,
}),
],
execute: async function ([limit = 3], context) {
let url = https://jsonplaceholder.typicode.com/posts?_limit=${limit};
let response = await context.fetcher.fetch({
method: “GET”,
url: url,
});

  // Returns the posts fetched from the API
  return response.body.map(post => ({
    id: post.id,
    title: post.title,
    body: post.body,
  }));
},

},
});

  1. The API works correctly:
    I tested the URL manually using tools like Postman and cURL, and it returns the expected data.
  2. Is this related to Coda’s plan limitations?
    Could it be that automatic table synchronization requires a paid plan?
  3. What might be wrong with the code?
    If there’s an issue with the schema, identityName, or the data being returned, I’d like to understand how to fix it.

Question:
• Why doesn’t the table populate when syncing?
• Is this related to Coda’s plan limitations, or is there something wrong with my code?
Thank you in advance for your help!


You should return an object with a result property, like this:

return {
  result: response.body.map...
};
1 Like

Yeah or just:

let rows = response.body.rows;
return {
  result: rows
}

Hey @Scott_Collier-Weir and @Leandro_Zubrezki

I have a similar problem and have been at it for several days. I just cant for the life of me make it work. I did try to implement your suggestions but it just hast worked for me.

I have tried to just dump the data, map the data, push it. I did work, but I can find the version that forks from so many improvement attempts.

I just cant get my pack to write to the syctable. I´m likely on version 200 of this thing.

In the middle of trying to implement a way to loop to get the 5K lines of data and deal with the timeout I completely lost it.

Could you please be so kind to give me a hand here with this code.

I can retrieve the data to the log, just cont write to the table. Thank you very much.

import * as coda from "@codahq/packs-sdk";

/**
 * This Coda Pack integrates with the Omie ERP system to retrieve client information.
 * It provides a sync table ("OmieClients") that fetches client data from Omie using the Omie API.
 * The pack uses App Key and App Secret authentication (no OAuth2).
 * It automatically retrieves all client records across multiple pages, handling pagination.
 *
 * To use this pack, the user must:
 * 1. Install the pack in their Coda document.
 * 2. Provide their Omie App Key and App Secret when syncing the table.
 * 3. Add the "OmieClients" sync table to their document.
 * 4. Run the "SyncOmieClients" formula to load client data from Omie into the table.
 */

// --- Pack Definition ---
export const pack = coda.newPack(); // Creates a new Coda Pack instance

// --- Network Domain ---
pack.addNetworkDomain("omie.com.br"); // Specifies the allowed network domain for API calls, enhancing security

// --- API Endpoint ---
const OMIE_CLIENTS_API_ENDPOINT = "https://app.omie.com.br/api/v1/geral/clientes/"; // Defines the API endpoint for retrieving client data from Omie

// --- Fetch Clients Function ---
async function fetchOmieClients(context, appKey, appSecret, page = 1) {
  let requestBody = {
    call: "ListarClientes", // Specifies the API method to call: "ListarClientes" (List Clients)
    app_key: appKey, // Uses the provided App Key
    app_secret: appSecret, // Uses the provided App Secret
    param: [
      {
        pagina: page, // Specifies the page number for pagination (defaults to 1)
        registros_por_pagina: 100 // Specifies the number of records per page (100 in this case)
      }
    ]
  };

  let response = await context.fetcher.fetch({
    url: OMIE_CLIENTS_API_ENDPOINT,
    method: "POST", // Specifies the HTTP method as POST
    headers: {
      "Content-Type": "application/json" // Sets the content type to JSON
    },
    body: JSON.stringify(requestBody) // Converts the request body to a JSON string
  });

  if (response.status !== 200) {
    console.error("Omie API Error:", response); // Logs the error to the console
    throw new Error(`Omie API Error: ${response.status} - ${response.body}`); // Throws an error with the status code and response body
  }

  let result = response.body;

  if (result.fault) {
    throw new Error(`Omie Error: ${result.fault.faultstring}`); // Throws an error with the error message from Omie
  }

  return result; // Returns the full response object (including the body)
}

// --- Client Schema ---
let ClientSchema = coda.makeObjectSchema({
  properties: {
    ID_ClienteOmie: { type: coda.ValueType.Number, description: "Omie's Client ID.", required: true, fromKey: "codigo_cliente_omie" },
    Codigo_Cliente: { type: coda.ValueType.String, description: "Client Code.", fromKey: "codigo_cliente_integracao" },
    Razao_Social: { type: coda.ValueType.String, description: "Client's Legal Name.", required: true, fromKey: "razao_social" },
    Nome_Fantasia: { type: coda.ValueType.String, description: "Client's Trade Name.", fromKey: "nome_fantasia" },
    CNPJ: { type: coda.ValueType.String, description: "Client's CNPJ.", fromKey: "cnpj_cpf" },
    CPF: { type: coda.ValueType.String, description: "Client's CPF.", fromKey: "cnpj_cpf" },
    Endereco: { type: coda.ValueType.String, description: "Client's Address.", fromKey: "endereco" },
    Numero: { type: coda.ValueType.String, description: "Address Number.", fromKey: "endereco_numero" },
    Complemento: { type: coda.ValueType.String, description: "Address Complement.", fromKey: "complemento" },
    Bairro: { type: coda.ValueType.String, description: "Neighborhood.", fromKey: "bairro" },
    CEP: { type: coda.ValueType.String, description: "Postal Code.", fromKey: "cep" },
    Cidade: { type: coda.ValueType.String, description: "City.", fromKey: "cidade" },
    Estado: { type: coda.ValueType.String, description: "State (e.g., SP).", fromKey: "estado" },
    Telefone: { type: coda.ValueType.String, description: "Primary Phone Number.", fromKey: "telefone1_numero" },
    Email: { type: coda.ValueType.String, description: "Email Address.", fromKey: "email" },
    Inativo: { type: coda.ValueType.Boolean, description: "Is the client inactive?", fromKey: "inativo" },
    Observacao: { type: coda.ValueType.String, description: "Additional notes.", fromKey: "observacao" },
    //Tags: {
    //  type: coda.ValueType.Array,
   //   description: "Tags associated with the client.",
   //   items: { type: coda.ValueType.String, codaType: coda.ValueHintType.SelectList, options: ["Fornecedor", "Cliente", "Parceiro", "Outro"] },
   //   fromKey: "tags"
   // },
    Contato: { type: coda.ValueType.String, description: "Contact person.", fromKey: "contato" },
    BloquearFaturamento: { type: coda.ValueType.Boolean, description: "Is invoicing blocked?", fromKey: "bloquear_faturamento" },
    CidadeIbge: { type: coda.ValueType.String, description: "City IBGE Code.", fromKey: "cidade_ibge" },
    DataCadastro: { type: coda.ValueType.String, description: "Date of registration.", fromKey: "data_cadastro" },
    DataAlteracao: { type: coda.ValueType.String, description: "Date of last modification.", fromKey: "data_alteracao" },
    Telefone2Numero: { type: coda.ValueType.String, description: "Secondary Phone Number.", fromKey: "telefone2_numero" },
    Telefone2DDD: { type: coda.ValueType.String, description: "Secondary Phone Number DDD.", fromKey: "telefone2_ddd" },
    Fax: { type: coda.ValueType.String, description: "Fax Number.", fromKey: "fax" },
    Website: { type: coda.ValueType.String, description: "Website URL.", fromKey: "website" },
    TipoPessoa: { type: coda.ValueType.String, description: "Type of Person (Física/Jurídica).", fromKey: "tipo_pessoa" },
    InscricaoEstadual: { type: coda.ValueType.String, description: "State Registration.", fromKey: "inscricao_estadual" },
    InscricaoMunicipal: { type: coda.ValueType.String, description: "Municipal Registration.", fromKey: "inscricao_municipal" }
  },
  displayProperty: "Razao_Social", // Specifies the property to display in Coda (Legal Name)
  idProperty: "ID_ClienteOmie", // Specifies the property to use as the unique ID (Omie's Client ID)
  identityName: "OmieClients" // Unique identity for this schema, needed for SyncTables
});

// --- Sync Table ---
pack.addSyncTable({
  name: "OmieClients",
  description: "Retrieves client data from Omie.",
  schema: ClientSchema,
  identityName: "OmieClients",
  formula: {
    name: "SyncOmieClients",
    description: "Syncs clients from Omie. Provide App Key and App Secret.",
    parameters: [
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "appKey",
        description: "Your Omie App Key."
      }),
      coda.makeParameter({
        type: coda.ParameterType.String,
        name: "appSecret",
        description: "Your Omie App Secret."
      })
    ],
    async execute([appKey, appSecret], context) {
      let allClients = [];
      let page = 1;
      let totalPages = 1;

      // Fetch the first page to get the total number of pages
      let firstResponse = await fetchOmieClients(context, appKey, appSecret, page);

      if (!firstResponse || !firstResponse.body) {
        return { result: [] }; // Return an empty array if no data is found
      }

      // Get the total number of pages from the first response
      if (firstResponse.body.total_de_paginas) {
        totalPages = firstResponse.body.total_de_paginas;
      } else {
        console.error("Unexpected response format:", firstResponse);
        throw new Error("Unexpected response format: 'total_de_paginas' not found");
      }

      // Loop through all pages
      for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
        let response = await fetchOmieClients(context, appKey, appSecret, currentPage);

        if (!response || !response.body || !response.body.clientes_cadastro) {
          continue; // Skip to the next page if there's an error or no data
        }

        // Push the mapped clients to the allClients array
        allClients.push(...response.body.clientes_cadastro.map(client => ({
          ID_ClienteOmie: client.codigo_cliente_omie,
          Codigo_Cliente: client.codigo_cliente_integracao,
          Razao_Social: client.razao_social,
          Nome_Fantasia: client.nome_fantasia,
          CNPJ: client.cnpj_cpf,
          CPF: client.cnpj_cpf,
          Endereco: client.endereco,
          Numero: client.endereco_numero,
          Complemento: client.complemento,
          Bairro: client.bairro,
          CEP: client.cep,
          Cidade: client.cidade,
          Estado: client.estado,
          Telefone: client.telefone1_numero,
          Email: client.email,
          Inativo: client.inativo === "S",
          Observacao: client.observacao,
          Tags: client.tags ? client.tags.map(tag => tag.tag) : [],
          Contato: client.contato,
          BloquearFaturamento: client.bloquear_faturamento === "S",
          CidadeIbge: client.cidade_ibge,
          DataCadastro: client.data_cadastro,
          DataAlteracao: client.data_alteracao,
          Telefone2Numero: client.telefone2_numero,
          Telefone2DDD: client.telefone2_ddd,
          Fax: client.fax,
          Website: client.website,
          TipoPessoa: client.tipo_pessoa,
          InscricaoEstadual: client.inscricao_estadual,
          InscricaoMunicipal: client.inscricao_municipal
        })));
      }

      return { result: allClients }; // Return the array of all client data
    }
  }
});

This is a jus dump version:

async execute([appKey, appSecret], context) {
      let allClients = [];
      let page = 1;
      let totalPages = 1;

      // Fetch the first page to get the total number of pages
      let firstResponse = await fetchOmieClients(context, appKey, appSecret, page);

      if (!firstResponse || !firstResponse.body) {
        return { result: [] }; // Return an empty array if no data is found
      }

      // Get the total number of pages from the first response
      if (firstResponse.body.total_de_paginas) {
        totalPages = firstResponse.body.total_de_paginas;
      } else {
        console.error("Unexpected response format:", firstResponse);
        throw new Error("Unexpected response format: 'total_de_paginas' not found");
      }

      // Loop through all pages
      for (let currentPage = 1; currentPage <= totalPages; currentPage++) {
        let response = await fetchOmieClients(context, appKey, appSecret, currentPage);

        if (!response || !response.body || !response.body.clientes_cadastro) {
          continue; // Skip to the next page if there's an error or no data
        }

        // Push the retrieved clients to the allClients array
       //let allClientss[] = response.body.clientes_cadastro;

        return { result: response.body.clientes_cadastro}; // Return the array of all client data

      }

      
    }
  }
});

Thak so much once again…

1 Like

Hi
you can use continuation :

execute: async function ([], context) {
      let page: number = (context.sync.continuation?.page as number) || 1;
      const url: string = `${BASE_URL}/v2/invoices?page=${page}`;
      const res = await myFunctionFetch(url, headers, .... etc);
      // I map to link 2 tables together
      const results = res.body.invoices.map(invoice => linkInvoiceToCustomer(invoice));
      // else just
      const results = res.body.invoices
      // set page or undefined if undefined the function will not be relaunched
      const continuation = res.body.total_pages > page ? { page: page + 1 } : undefined;

      return {
        result: results,
        continuation
      };
    }
4 Likes

Thanks @Math_24 Will give it a try!

1 Like