diff --git a/packages/backend/src/apps/airtable/actions/create-record/index.js b/packages/backend/src/apps/airtable/actions/create-record/index.js
new file mode 100644
index 0000000000..554015bea9
--- /dev/null
+++ b/packages/backend/src/apps/airtable/actions/create-record/index.js
@@ -0,0 +1,92 @@
+import defineAction from '../../../../helpers/define-action.js';
+
+export default defineAction({
+ name: 'Create record',
+ key: 'createRecord',
+ description: 'Creates a new record with fields that automatically populate.',
+ arguments: [
+ {
+ label: 'Base',
+ key: 'baseId',
+ type: 'dropdown',
+ required: true,
+ description: 'Base in which to create the record.',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listBases',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Table',
+ key: 'tableId',
+ type: 'dropdown',
+ required: true,
+ dependsOn: ['parameters.baseId'],
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listTables',
+ },
+ {
+ name: 'parameters.baseId',
+ value: '{parameters.baseId}',
+ },
+ ],
+ },
+ additionalFields: {
+ type: 'query',
+ name: 'getDynamicFields',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listFields',
+ },
+ {
+ name: 'parameters.baseId',
+ value: '{parameters.baseId}',
+ },
+ {
+ name: 'parameters.tableId',
+ value: '{parameters.tableId}',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const { baseId, tableId, ...rest } = $.step.parameters;
+
+ const fields = Object.entries(rest).reduce((result, [key, value]) => {
+ if (Array.isArray(value)) {
+ result[key] = value.map((item) => item.value);
+ } else if (value !== '') {
+ result[key] = value;
+ }
+ return result;
+ }, {});
+
+ const body = {
+ typecast: true,
+ fields,
+ };
+
+ const { data } = await $.http.post(`/v0/${baseId}/${tableId}`, body);
+
+ $.setActionItem({
+ raw: data,
+ });
+ },
+});
diff --git a/packages/backend/src/apps/airtable/actions/find-record/index.js b/packages/backend/src/apps/airtable/actions/find-record/index.js
new file mode 100644
index 0000000000..ad0f1ea74a
--- /dev/null
+++ b/packages/backend/src/apps/airtable/actions/find-record/index.js
@@ -0,0 +1,174 @@
+import defineAction from '../../../../helpers/define-action.js';
+import { URLSearchParams } from 'url';
+
+export default defineAction({
+ name: 'Find record',
+ key: 'findRecord',
+ description:
+ "Finds a record using simple field search or use Airtable's formula syntax to find a matching record.",
+ arguments: [
+ {
+ label: 'Base',
+ key: 'baseId',
+ type: 'dropdown',
+ required: true,
+ description: 'Base in which to create the record.',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listBases',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Table',
+ key: 'tableId',
+ type: 'dropdown',
+ required: true,
+ dependsOn: ['parameters.baseId'],
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listTables',
+ },
+ {
+ name: 'parameters.baseId',
+ value: '{parameters.baseId}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Search by field',
+ key: 'tableField',
+ type: 'dropdown',
+ required: false,
+ dependsOn: ['parameters.baseId', 'parameters.tableId'],
+ description: '',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listTableFields',
+ },
+ {
+ name: 'parameters.baseId',
+ value: '{parameters.baseId}',
+ },
+ {
+ name: 'parameters.tableId',
+ value: '{parameters.tableId}',
+ },
+ ],
+ },
+ },
+ {
+ label: 'Search Value',
+ key: 'searchValue',
+ type: 'string',
+ required: false,
+ variables: true,
+ description:
+ 'The value of unique identifier for the record. For date values, please use the ISO format (e.g., "YYYY-MM-DD").',
+ },
+ {
+ label: 'Search for exact match?',
+ key: 'exactMatch',
+ type: 'dropdown',
+ required: true,
+ description: '',
+ variables: true,
+ options: [
+ { label: 'Yes', value: 'true' },
+ { label: 'No', value: 'false' },
+ ],
+ },
+ {
+ label: 'Search Formula',
+ key: 'searchFormula',
+ type: 'string',
+ required: false,
+ variables: true,
+ description:
+ 'Instead, you have the option to use an Airtable search formula for locating records according to sophisticated criteria and across various fields.',
+ },
+ {
+ label: 'Limit to View',
+ key: 'limitToView',
+ type: 'dropdown',
+ required: false,
+ dependsOn: ['parameters.baseId', 'parameters.tableId'],
+ description:
+ 'You have the choice to restrict the search to a particular view ID if desired.',
+ variables: true,
+ source: {
+ type: 'query',
+ name: 'getDynamicData',
+ arguments: [
+ {
+ name: 'key',
+ value: 'listTableViews',
+ },
+ {
+ name: 'parameters.baseId',
+ value: '{parameters.baseId}',
+ },
+ {
+ name: 'parameters.tableId',
+ value: '{parameters.tableId}',
+ },
+ ],
+ },
+ },
+ ],
+
+ async run($) {
+ const {
+ baseId,
+ tableId,
+ tableField,
+ searchValue,
+ exactMatch,
+ searchFormula,
+ limitToView,
+ } = $.step.parameters;
+
+ let filterByFormula;
+
+ if (tableField && searchValue) {
+ filterByFormula =
+ exactMatch === 'true'
+ ? `{${tableField}} = '${searchValue}'`
+ : `LOWER({${tableField}}) = LOWER('${searchValue}')`;
+ } else {
+ filterByFormula = searchFormula;
+ }
+
+ const body = new URLSearchParams({
+ filterByFormula,
+ view: limitToView,
+ });
+
+ const { data } = await $.http.post(
+ `/v0/${baseId}/${tableId}/listRecords`,
+ body
+ );
+
+ $.setActionItem({
+ raw: data,
+ });
+ },
+});
diff --git a/packages/backend/src/apps/airtable/actions/index.js b/packages/backend/src/apps/airtable/actions/index.js
new file mode 100644
index 0000000000..bb3c63ce9b
--- /dev/null
+++ b/packages/backend/src/apps/airtable/actions/index.js
@@ -0,0 +1,4 @@
+import createRecord from './create-record/index.js';
+import findRecord from './find-record/index.js';
+
+export default [createRecord, findRecord];
diff --git a/packages/backend/src/apps/airtable/assets/favicon.svg b/packages/backend/src/apps/airtable/assets/favicon.svg
new file mode 100644
index 0000000000..867c3b5aef
--- /dev/null
+++ b/packages/backend/src/apps/airtable/assets/favicon.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/packages/backend/src/apps/airtable/auth/generate-auth-url.js b/packages/backend/src/apps/airtable/auth/generate-auth-url.js
new file mode 100644
index 0000000000..70d5d7e351
--- /dev/null
+++ b/packages/backend/src/apps/airtable/auth/generate-auth-url.js
@@ -0,0 +1,38 @@
+import crypto from 'crypto';
+import { URLSearchParams } from 'url';
+import authScope from '../common/auth-scope.js';
+
+export default async function generateAuthUrl($) {
+ const oauthRedirectUrlField = $.app.auth.fields.find(
+ (field) => field.key == 'oAuthRedirectUrl'
+ );
+ const redirectUri = oauthRedirectUrlField.value;
+ const state = crypto.randomBytes(100).toString('base64url');
+ const codeVerifier = crypto.randomBytes(96).toString('base64url');
+ const codeChallenge = crypto
+ .createHash('sha256')
+ .update(codeVerifier)
+ .digest('base64')
+ .replace(/=/g, '')
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_');
+
+ const searchParams = new URLSearchParams({
+ client_id: $.auth.data.clientId,
+ redirect_uri: redirectUri,
+ response_type: 'code',
+ scope: authScope.join(' '),
+ state,
+ code_challenge: codeChallenge,
+ code_challenge_method: 'S256',
+ });
+
+ const url = `https://airtable.com/oauth2/v1/authorize?${searchParams.toString()}`;
+
+ await $.auth.set({
+ url,
+ originalCodeChallenge: codeChallenge,
+ originalState: state,
+ codeVerifier,
+ });
+}
diff --git a/packages/backend/src/apps/airtable/auth/index.js b/packages/backend/src/apps/airtable/auth/index.js
new file mode 100644
index 0000000000..6422369f56
--- /dev/null
+++ b/packages/backend/src/apps/airtable/auth/index.js
@@ -0,0 +1,48 @@
+import generateAuthUrl from './generate-auth-url.js';
+import verifyCredentials from './verify-credentials.js';
+import refreshToken from './refresh-token.js';
+import isStillVerified from './is-still-verified.js';
+
+export default {
+ fields: [
+ {
+ key: 'oAuthRedirectUrl',
+ label: 'OAuth Redirect URL',
+ type: 'string',
+ required: true,
+ readOnly: true,
+ value: '{WEB_APP_URL}/app/airtable/connections/add',
+ placeholder: null,
+ description:
+ 'When asked to input a redirect URL in Airtable, enter the URL above.',
+ clickToCopy: true,
+ },
+ {
+ key: 'clientId',
+ label: 'Client ID',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ {
+ key: 'clientSecret',
+ label: 'Client Secret',
+ type: 'string',
+ required: true,
+ readOnly: false,
+ value: null,
+ placeholder: null,
+ description: null,
+ clickToCopy: false,
+ },
+ ],
+
+ generateAuthUrl,
+ verifyCredentials,
+ isStillVerified,
+ refreshToken,
+};
diff --git a/packages/backend/src/apps/airtable/auth/is-still-verified.js b/packages/backend/src/apps/airtable/auth/is-still-verified.js
new file mode 100644
index 0000000000..0896289546
--- /dev/null
+++ b/packages/backend/src/apps/airtable/auth/is-still-verified.js
@@ -0,0 +1,8 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const isStillVerified = async ($) => {
+ const currentUser = await getCurrentUser($);
+ return !!currentUser.id;
+};
+
+export default isStillVerified;
diff --git a/packages/backend/src/apps/airtable/auth/refresh-token.js b/packages/backend/src/apps/airtable/auth/refresh-token.js
new file mode 100644
index 0000000000..6f73550096
--- /dev/null
+++ b/packages/backend/src/apps/airtable/auth/refresh-token.js
@@ -0,0 +1,40 @@
+import { URLSearchParams } from 'node:url';
+
+import authScope from '../common/auth-scope.js';
+
+const refreshToken = async ($) => {
+ const params = new URLSearchParams({
+ client_id: $.auth.data.clientId,
+ grant_type: 'refresh_token',
+ refresh_token: $.auth.data.refreshToken,
+ });
+
+ const basicAuthToken = Buffer.from(
+ $.auth.data.clientId + ':' + $.auth.data.clientSecret
+ ).toString('base64');
+
+ const { data } = await $.http.post(
+ 'https://airtable.com/oauth2/v1/token',
+ params.toString(),
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Authorization: `Basic ${basicAuthToken}`,
+ },
+ additionalProperties: {
+ skipAddingAuthHeader: true,
+ },
+ }
+ );
+
+ await $.auth.set({
+ accessToken: data.access_token,
+ refreshToken: data.refresh_token,
+ expiresIn: data.expires_in,
+ refreshExpiresIn: data.refresh_expires_in,
+ scope: authScope.join(' '),
+ tokenType: data.token_type,
+ });
+};
+
+export default refreshToken;
diff --git a/packages/backend/src/apps/airtable/auth/verify-credentials.js b/packages/backend/src/apps/airtable/auth/verify-credentials.js
new file mode 100644
index 0000000000..f2ef8115d2
--- /dev/null
+++ b/packages/backend/src/apps/airtable/auth/verify-credentials.js
@@ -0,0 +1,56 @@
+import getCurrentUser from '../common/get-current-user.js';
+
+const verifyCredentials = async ($) => {
+ if ($.auth.data.originalState !== $.auth.data.state) {
+ throw new Error("The 'state' parameter does not match.");
+ }
+ if ($.auth.data.originalCodeChallenge !== $.auth.data.code_challenge) {
+ throw new Error("The 'code challenge' parameter does not match.");
+ }
+ const oauthRedirectUrlField = $.app.auth.fields.find(
+ (field) => field.key == 'oAuthRedirectUrl'
+ );
+ const redirectUri = oauthRedirectUrlField.value;
+ const basicAuthToken = Buffer.from(
+ $.auth.data.clientId + ':' + $.auth.data.clientSecret
+ ).toString('base64');
+
+ const { data } = await $.http.post(
+ 'https://airtable.com/oauth2/v1/token',
+ {
+ code: $.auth.data.code,
+ client_id: $.auth.data.clientId,
+ redirect_uri: redirectUri,
+ grant_type: 'authorization_code',
+ code_verifier: $.auth.data.codeVerifier,
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/x-www-form-urlencoded',
+ Authorization: `Basic ${basicAuthToken}`,
+ },
+ additionalProperties: {
+ skipAddingAuthHeader: true,
+ },
+ }
+ );
+
+ await $.auth.set({
+ accessToken: data.access_token,
+ tokenType: data.token_type,
+ });
+
+ const currentUser = await getCurrentUser($);
+
+ await $.auth.set({
+ clientId: $.auth.data.clientId,
+ clientSecret: $.auth.data.clientSecret,
+ scope: $.auth.data.scope,
+ expiresIn: data.expires_in,
+ refreshExpiresIn: data.refresh_expires_in,
+ refreshToken: data.refresh_token,
+ screenName: currentUser.email,
+ });
+};
+
+export default verifyCredentials;
diff --git a/packages/backend/src/apps/airtable/common/add-auth-header.js b/packages/backend/src/apps/airtable/common/add-auth-header.js
new file mode 100644
index 0000000000..f957ebf964
--- /dev/null
+++ b/packages/backend/src/apps/airtable/common/add-auth-header.js
@@ -0,0 +1,12 @@
+const addAuthHeader = ($, requestConfig) => {
+ if (
+ !requestConfig.additionalProperties?.skipAddingAuthHeader &&
+ $.auth.data?.accessToken
+ ) {
+ requestConfig.headers.Authorization = `${$.auth.data.tokenType} ${$.auth.data.accessToken}`;
+ }
+
+ return requestConfig;
+};
+
+export default addAuthHeader;
diff --git a/packages/backend/src/apps/airtable/common/auth-scope.js b/packages/backend/src/apps/airtable/common/auth-scope.js
new file mode 100644
index 0000000000..8b4cbca801
--- /dev/null
+++ b/packages/backend/src/apps/airtable/common/auth-scope.js
@@ -0,0 +1,12 @@
+const authScope = [
+ 'data.records:read',
+ 'data.records:write',
+ 'data.recordComments:read',
+ 'data.recordComments:write',
+ 'schema.bases:read',
+ 'schema.bases:write',
+ 'user.email:read',
+ 'webhook:manage',
+];
+
+export default authScope;
diff --git a/packages/backend/src/apps/airtable/common/get-current-user.js b/packages/backend/src/apps/airtable/common/get-current-user.js
new file mode 100644
index 0000000000..c04f16a958
--- /dev/null
+++ b/packages/backend/src/apps/airtable/common/get-current-user.js
@@ -0,0 +1,6 @@
+const getCurrentUser = async ($) => {
+ const { data: currentUser } = await $.http.get('/v0/meta/whoami');
+ return currentUser;
+};
+
+export default getCurrentUser;
diff --git a/packages/backend/src/apps/airtable/dynamic-data/index.js b/packages/backend/src/apps/airtable/dynamic-data/index.js
new file mode 100644
index 0000000000..c12f341f99
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-data/index.js
@@ -0,0 +1,6 @@
+import listBases from './list-bases/index.js';
+import listTableFields from './list-table-fields/index.js';
+import listTableViews from './list-table-views/index.js';
+import listTables from './list-tables/index.js';
+
+export default [listBases, listTableFields, listTableViews, listTables];
diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js
new file mode 100644
index 0000000000..2f075694ad
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-data/list-bases/index.js
@@ -0,0 +1,28 @@
+export default {
+ name: 'List bases',
+ key: 'listBases',
+
+ async run($) {
+ const bases = {
+ data: [],
+ };
+
+ const params = {};
+
+ do {
+ const { data } = await $.http.get('/v0/meta/bases', { params });
+ params.offset = data.offset;
+
+ if (data?.bases) {
+ for (const base of data.bases) {
+ bases.data.push({
+ value: base.id,
+ name: base.name,
+ });
+ }
+ }
+ } while (params.offset);
+
+ return bases;
+ },
+};
diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js
new file mode 100644
index 0000000000..3f96d127d3
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-data/list-table-fields/index.js
@@ -0,0 +1,39 @@
+export default {
+ name: 'List table fields',
+ key: 'listTableFields',
+
+ async run($) {
+ const tableFields = {
+ data: [],
+ };
+ const { baseId, tableId } = $.step.parameters;
+
+ if (!baseId) {
+ return tableFields;
+ }
+
+ const params = {};
+
+ do {
+ const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
+ params,
+ });
+ params.offset = data.offset;
+
+ if (data?.tables) {
+ for (const table of data.tables) {
+ if (table.id === tableId) {
+ table.fields.forEach((field) => {
+ tableFields.data.push({
+ value: field.name,
+ name: field.name,
+ });
+ });
+ }
+ }
+ }
+ } while (params.offset);
+
+ return tableFields;
+ },
+};
diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js
new file mode 100644
index 0000000000..d2ec912757
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-data/list-table-views/index.js
@@ -0,0 +1,39 @@
+export default {
+ name: 'List table views',
+ key: 'listTableViews',
+
+ async run($) {
+ const tableViews = {
+ data: [],
+ };
+ const { baseId, tableId } = $.step.parameters;
+
+ if (!baseId) {
+ return tableViews;
+ }
+
+ const params = {};
+
+ do {
+ const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
+ params,
+ });
+ params.offset = data.offset;
+
+ if (data?.tables) {
+ for (const table of data.tables) {
+ if (table.id === tableId) {
+ table.views.forEach((view) => {
+ tableViews.data.push({
+ value: view.id,
+ name: view.name,
+ });
+ });
+ }
+ }
+ }
+ } while (params.offset);
+
+ return tableViews;
+ },
+};
diff --git a/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js b/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js
new file mode 100644
index 0000000000..90d6b4c0fa
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-data/list-tables/index.js
@@ -0,0 +1,35 @@
+export default {
+ name: 'List tables',
+ key: 'listTables',
+
+ async run($) {
+ const tables = {
+ data: [],
+ };
+ const baseId = $.step.parameters.baseId;
+
+ if (!baseId) {
+ return tables;
+ }
+
+ const params = {};
+
+ do {
+ const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`, {
+ params,
+ });
+ params.offset = data.offset;
+
+ if (data?.tables) {
+ for (const table of data.tables) {
+ tables.data.push({
+ value: table.id,
+ name: table.name,
+ });
+ }
+ }
+ } while (params.offset);
+
+ return tables;
+ },
+};
diff --git a/packages/backend/src/apps/airtable/dynamic-fields/index.js b/packages/backend/src/apps/airtable/dynamic-fields/index.js
new file mode 100644
index 0000000000..5d97313ea0
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-fields/index.js
@@ -0,0 +1,3 @@
+import listFields from './list-fields/index.js';
+
+export default [listFields];
diff --git a/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js b/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js
new file mode 100644
index 0000000000..704d194054
--- /dev/null
+++ b/packages/backend/src/apps/airtable/dynamic-fields/list-fields/index.js
@@ -0,0 +1,86 @@
+const hasValue = (value) => value !== null && value !== undefined;
+
+export default {
+ name: 'List fields',
+ key: 'listFields',
+
+ async run($) {
+ const options = [];
+ const { baseId, tableId } = $.step.parameters;
+
+ if (!hasValue(baseId) || !hasValue(tableId)) {
+ return;
+ }
+
+ const { data } = await $.http.get(`/v0/meta/bases/${baseId}/tables`);
+
+ const selectedTable = data.tables.find((table) => table.id === tableId);
+
+ if (!selectedTable) return;
+
+ selectedTable.fields.forEach((field) => {
+ if (field.type === 'singleSelect') {
+ options.push({
+ label: field.name,
+ key: field.name,
+ type: 'dropdown',
+ required: false,
+ variables: true,
+ options: field.options.choices.map((choice) => ({
+ label: choice.name,
+ value: choice.id,
+ })),
+ });
+ } else if (field.type === 'multipleSelects') {
+ options.push({
+ label: field.name,
+ key: field.name,
+ type: 'dynamic',
+ required: false,
+ variables: true,
+ fields: [
+ {
+ label: 'Value',
+ key: 'value',
+ type: 'dropdown',
+ required: false,
+ variables: true,
+ options: field.options.choices.map((choice) => ({
+ label: choice.name,
+ value: choice.id,
+ })),
+ },
+ ],
+ });
+ } else if (field.type === 'checkbox') {
+ options.push({
+ label: field.name,
+ key: field.name,
+ type: 'dropdown',
+ required: false,
+ variables: true,
+ options: [
+ {
+ label: 'Yes',
+ value: 'true',
+ },
+ {
+ label: 'No',
+ value: 'false',
+ },
+ ],
+ });
+ } else {
+ options.push({
+ label: field.name,
+ key: field.name,
+ type: 'string',
+ required: false,
+ variables: true,
+ });
+ }
+ });
+
+ return options;
+ },
+};
diff --git a/packages/backend/src/apps/airtable/index.js b/packages/backend/src/apps/airtable/index.js
new file mode 100644
index 0000000000..81ecaf7890
--- /dev/null
+++ b/packages/backend/src/apps/airtable/index.js
@@ -0,0 +1,22 @@
+import defineApp from '../../helpers/define-app.js';
+import addAuthHeader from './common/add-auth-header.js';
+import auth from './auth/index.js';
+import actions from './actions/index.js';
+import dynamicData from './dynamic-data/index.js';
+import dynamicFields from './dynamic-fields/index.js';
+
+export default defineApp({
+ name: 'Airtable',
+ key: 'airtable',
+ baseUrl: 'https://airtable.com',
+ apiBaseUrl: 'https://api.airtable.com',
+ iconUrl: '{BASE_URL}/apps/airtable/assets/favicon.svg',
+ authDocUrl: '{DOCS_URL}/apps/airtable/connection',
+ primaryColor: 'FFBF00',
+ supportsConnections: true,
+ beforeRequest: [addAuthHeader],
+ auth,
+ actions,
+ dynamicData,
+ dynamicFields,
+});
diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js
index 7d49637e3d..462fec4147 100644
--- a/packages/docs/pages/.vitepress/config.js
+++ b/packages/docs/pages/.vitepress/config.js
@@ -26,12 +26,21 @@ export default defineConfig({
},
{
text: 'Apps',
- link: '/apps/carbone/connection',
+ link: '/apps/airtable/connection',
activeMatch: '/apps/',
},
],
sidebar: {
'/apps/': [
+ {
+ text: 'Airtable',
+ collapsible: true,
+ collapsed: true,
+ items: [
+ { text: 'Actions', link: '/apps/airtable/actions' },
+ { text: 'Connection', link: '/apps/airtable/connection' },
+ ],
+ },
{
text: 'Carbone',
collapsible: true,
diff --git a/packages/docs/pages/apps/airtable/actions.md b/packages/docs/pages/apps/airtable/actions.md
new file mode 100644
index 0000000000..432e0d4656
--- /dev/null
+++ b/packages/docs/pages/apps/airtable/actions.md
@@ -0,0 +1,14 @@
+---
+favicon: /favicons/airtable.svg
+items:
+ - name: Create record
+ desc: Creates a new record with fields that automatically populate.
+ - name: Find record
+ desc: Finds a record using simple field search or use Airtable's formula syntax to find a matching record.
+---
+
+
+
+
diff --git a/packages/docs/pages/apps/airtable/connection.md b/packages/docs/pages/apps/airtable/connection.md
new file mode 100644
index 0000000000..57bcd78747
--- /dev/null
+++ b/packages/docs/pages/apps/airtable/connection.md
@@ -0,0 +1,19 @@
+# Airtable
+
+:::info
+This page explains the steps you need to follow to set up the Airtable
+connection in Automatisch. If any of the steps are outdated, please let us know!
+:::
+
+1. Login to your [Airtable account](https://www.airtable.com/).
+2. Go to this [link](https://airtable.com/create/oauth) and click on the **Register new OAuth integration**.
+3. Fill the name field.
+4. Copy **OAuth Redirect URL** from Automatisch to **OAuth redirect URL** field.
+5. Click on the **Register integration** button.
+6. In **Developer Details** section, click on the **Generate client secret**.
+7. Check the checkboxes of **Scopes** section.
+8. Click on the **Save changes** button.
+9. Copy **Client ID** to **Client ID** field on Automatisch.
+10. Copy **Client secret** to **Client secret** field on Automatisch.
+11. Click **Submit** button on Automatisch.
+12. Congrats! Start using your new Airtable connection within the flows.
diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md
index 73931c57de..40d2ad186c 100644
--- a/packages/docs/pages/guide/available-apps.md
+++ b/packages/docs/pages/guide/available-apps.md
@@ -2,6 +2,7 @@
The following integrations are currently supported by Automatisch.
+- [Airtable](/apps/airtable/actions)
- [Carbone](/apps/carbone/actions)
- [Datastore](/apps/datastore/actions)
- [DeepL](/apps/deepl/actions)
diff --git a/packages/docs/pages/public/favicons/airtable.svg b/packages/docs/pages/public/favicons/airtable.svg
new file mode 100644
index 0000000000..867c3b5aef
--- /dev/null
+++ b/packages/docs/pages/public/favicons/airtable.svg
@@ -0,0 +1,9 @@
+
+