Appsmith — Open-Source Internal Tool Builder
Overview
Appsmith is an open-source low-code platform for internal tools: widgets are dragged onto a canvas, queries talk to databases and APIs, and JavaScript ({{ }} bindings and JSObjects) ties them together. It runs on Appsmith Cloud or self-hosted as a single container that bundles the server, MongoDB, Redis and PostgreSQL. Two images exist: appsmith-ce (Community, Apache-2.0) and appsmith-ee (Commercial edition with a free plan; Business and Enterprise features unlock with a license key). This skill reflects Appsmith v2.4 (September 2026).
Instructions
Data Queries
-- OrdersQuery (PostgreSQL). Bindings read widget values; with prepared statements on, each one is a parameter.
-- Never put a binding inside a SQL comment: it is still counted as a parameter.
SELECT * FROM orders
WHERE status = {{ StatusSelect.selectedOptionValue }}
AND created_at BETWEEN {{ FromDate.selectedDate }} AND {{ ToDate.selectedDate }}
AND ({{ SearchInput.text }} = '' OR customer_email ILIKE '%' || {{ SearchInput.text }} || '%')
ORDER BY created_at DESC
LIMIT {{ OrdersTable.pageSize }} OFFSET {{ OrdersTable.pageOffset }}
-- UpdateOrderQuery: values passed at call time arrive as this.params
UPDATE orders SET status = {{ this.params.status }} WHERE id = {{ this.params.orderId }}
UpdatePlanAPI (REST API datasource; auth headers live in the datasource)
PUT https://api.northwind.io/users/{{ UsersTable.selectedRow.id }}
Body: { "plan": {{ PlanSelect.selectedOptionValue }}, "note": {{ NoteInput.text }} }
Widget properties used most: Table selectedRow, pageNo, pageSize, pageOffset, searchText; Select selectedOptionValue; Input text; DatePicker selectedDate. For server-side pagination turn on the table's Server side pagination and run the query from onPageChange.
Each query has a Run behavior in its settings: Manual (only .run() or a widget event), On page load, or Automatic (re-runs whenever a bound value changes). Do not combine Automatic with an event handler that also calls .run() — the query executes twice. Other settings there: Use Prepared Statements (on by default), Request confirmation before running, and Query timeout (default 10 000 ms, maximum 60 000 ms).
JavaScript Objects (JSObjects)
// JSObject "Refunds" — reusable business logic
export default {
// Transform query data for charts (Lodash `_` and `moment` are built in)
getRevenueByMonth() {
return OrdersQuery.data.reduce((acc, order) => {
const month = moment(order.created_at).format("YYYY-MM");
acc[month] = (acc[month] || 0) + order.amount;
return acc;
}, {});
},
// Multi-step action, run from the Confirm button inside ConfirmRefundModal
async processRefund() {
const order = OrdersTable.selectedRow; // {} when nothing is selected
if (!order.id) {
showAlert("Select an order first", "warning");
return;
}
try {
await StripeRefundAPI.run({ chargeId: order.stripe_charge_id }); // {{ this.params.chargeId }}
await UpdateOrderQuery.run({ orderId: order.id, status: "refunded" });
await SlackNotifyAPI.run({ message: `Refund processed for order #${order.id} ($${order.amount})` });
showAlert("Refund processed successfully", "success");
await OrdersQuery.run(); // refresh the table
} catch (error) {
showAlert(`Refund failed: ${error.message}`, "error");
} finally {
closeModal(ConfirmRefundModal.name);
}
},
// Form validation
validateForm() {
const errors = {};
if (!EmailInput.text?.includes("@")) errors.email = "Invalid email";
if (Number(AmountInput.text) <= 0) errors.amount = "Amount must be positive";
if (!ReasonSelect.selectedOptionValue) errors.reason = "Select a reason";
return errors;
},
};
Framework functions: Query.run(params) returns a promise with the response; showAlert(message, type) shows a 5-second toast (info, success, warning, error); showModal(Modal.name) and closeModal(Modal.name) open and close a Modal widget; storeValue(key, value, persist) writes to appsmith.store. showModal only opens the modal — it does not wait for an answer, so put the confirmed action on a button inside the modal (or use the query's confirmation setting).
Deployment
# ~/appsmith/docker-compose.yml — pin a release tag from github.com/appsmithorg/appsmith/releases
services:
appsmith:
image: index.docker.io/appsmith/appsmith-ee:v2.4.3 # or appsmith/appsmith-ce:v2.4.3
container_name: appsmith
ports:
- "80:80"
- "443:443"
volumes:
- ./stacks:/appsmith-stacks
restart: unless-stopped
docker compose up -d # first start takes up to 5 minutes; then open http://localhost
docker compose exec -it appsmith appsmithctl backup # encrypted archive in ./stacks/data/backup/
# settings: edit ./stacks/configuration/docker.env, then
docker compose restart appsmith
# Kubernetes with Helm (Commercial edition chart; the Community chart is at https://helm.appsmith.com)
helm repo add appsmith-ee https://helm-ee.appsmith.com && helm repo update
helm install appsmith-ee appsmith-ee/appsmith -n appsmith-ee --create-namespace -f values.yaml
kubectl get pods -n appsmith-ee
values.yaml must set image.tag and the ingress className and host; the Kubernetes guide also sets mongodb.enabled: false and enables mongodbCommunity and mongodbOperator, replacing the legacy Bitnami MongoDB subchart.
Git Sync
- Open the app and click Connect Git at the bottom left of the editor. Only SSH remotes work (HTTPS Git URLs are not supported), and the target repository must be empty.
- Paste the SSH URL, generate a key (ECDSA 256 or RSA 4096), and add it to the repository as a deploy key with write access.
- Work on a feature branch, commit and push from Appsmith, open a pull request, merge, then pull in Appsmith and deploy.
Branch protection, a per-instance default branch and automatic pull-and-deploy after a merge (a deploy URL called by CI with a bearer token) are Enterprise features in Git settings; without them someone pulls the branch in Appsmith by hand.
Examples
Example 1: Self-host Appsmith for a team
User: "Set up Appsmith on our Ubuntu server with Docker and make sure we can back it up."
mkdir -p ~/appsmith && cd ~/appsmith
# write the docker-compose.yml shown above with image tag v2.4.3
docker compose up -d
docker compose ps # the appsmith container should be listed as running
until curl -fsS http://localhost/api/v1/health >/dev/null; do sleep 10; done # server is up (up to 5 minutes)
docker compose exec -it appsmith appsmithctl backup
ls stacks/data/backup/ # appsmith-backup-*.tar.gz.enc
After a few minutes the server's address (http://localhost on the same machine) shows the setup page where the first visitor creates the administrator account, so open it right away. The backup prompts for a password and cannot be restored without it. To set a custom domain or disable telemetry, edit stacks/configuration/docker.env (APPSMITH_CUSTOM_DOMAIN, APPSMITH_DISABLE_TELEMETRY=true) and restart.
Example 2: Orders admin panel with a guarded refund
User: "Build an orders page: filter by status, search by email, and let support refund the selected order after confirming."
- Add the filter widgets
StatusSelect, FromDate, ToDate and SearchInput, then a Table OrdersTable with data {{ OrdersQuery.data }} and server-side pagination on.
- Add a PostgreSQL datasource and create
OrdersQuery and UpdateOrderQuery from the Data Queries section. Set OrdersQuery to Automatic so it re-runs when a filter or the table page changes — do not also call it from onPageChange. Create the REST queries StripeRefundAPI and SlackNotifyAPI (reading {{ this.params.chargeId }} and {{ this.params.message }}) and the Refunds JSObject from above.
- Add a "Refund" button with onClick
{{ showModal(ConfirmRefundModal.name) }} and Disabled {{ !OrdersTable.selectedRow.id || OrdersTable.selectedRow.status === 'refunded' }}.
- Add a Modal named
ConfirmRefundModal and set its Confirm button's onClick to {{ Refunds.processRefund() }}.
Selecting order 4812 and confirming runs the three queries in order, shows the toast "Refund processed successfully", reloads the table with the row's status now refunded and closes the modal. If Stripe rejects the refund, the toast reads "Refund failed: …", the order is not updated and the modal closes.
Guidelines
- Pin the image tag — without a tag Docker pulls
latest and a container recreate becomes an unplanned upgrade.
- Upgrade through the checkpoint — an instance older than v1.96 must first be upgraded to a v1.96–v1.99 release before moving to v2.x (v2 bundles MongoDB 7); instances older than v1.9.2 have an earlier checkpoint. Run
appsmithctl backup first.
- Keep prepared statements on — they are the SQL-injection defence. A binding is then a value, not SQL text, so it cannot supply a table or column name; build patterns with
'%' || {{ SearchInput.text }} || '%'.
- JSObjects for logic — keep multi-step logic in JSObjects rather than long inline
{{ }} handlers; use async/await with try/catch so a failed query stops the chain.
selectedRow is never null — it is {} when nothing is selected, so test a field (selectedRow.id), not the object.
- Mind the 10-second query timeout — raise it per query (up to 60 s); self-hosted instances can lift that 60 s ceiling with
APPSMITH_SERVER_TIMEOUT (in seconds). Move longer work out of the request.
- Credentials stay in datasources — never paste API keys into queries or JSObjects, where every app editor (and the Git repository) can read them.
- Paid features — multiple datasource environments, granular access control, audit logs, reusable Packages and Workflows need the Business plan; Git CI/CD needs Enterprise; the MCP server (beta) is for Business Cloud and self-hosted Enterprise.
- Appsmith AI is gone — the Appsmith AI datasource reached end of life on September 30, 2026; use an OpenAI, Anthropic or Google AI datasource instead.
- Network needs — self-hosted instances call
cs.appsmith.com; allow it in outbound firewall rules.