Appsmith: Internal Tool Builder That Actually Handles Complex Business Logic

The ops team was running customer support on spreadsheets. 50 support agents, 200 tickets/day, tracked in a shared Google Sheet with color-coded cells. It was a disaster. They needed a proper ticket management system. The IT team quoted 4 months. I built it in Appsmith in 3 days. Self-hosted, connected to their existing PostgreSQL database, with role-based views and automated escalations. Here's how.

Why Appsmith Over Retool for Self-Hosting

I compared Appsmith and Retool for this project. Key differences:

FeatureRetoolAppsmith
Self-hostedEnterprise plan only ($$$)Free (open-source)
Pricing modelPer user/monthFree self-hosted
Git version controlAvailableAvailable
JS in queriesFull JS supportFull JS support
Custom componentsReact componentsReact + HTML widgets
CommunitySmallerLarger (GitHub stars)

The deal-breaker was cost. 50 support agents on Retool would cost thousands per month. Appsmith self-hosted is free. The features are comparable. Appsmith wins for self-hosted use cases.

Connecting to Multiple Data Sources

The ticket system needed data from three places:

// In Appsmith, data sources are configured in the UI
// Each query runs against a specific data source

// Query: get_tickets (PostgreSQL)
// SELECT * FROM tickets
// WHERE assigned_to = {{appsmith.user.email}}
//   AND status IN ({{JSON.stringify(Table1.selectedRows.map(r => r.status))}})
// ORDER BY priority DESC, created_at ASC
// LIMIT 50

// Query: get_customer_from_crm (REST API)
// GET https://crm.example.com/api/customers/{{ticket.customer_id}}
// Headers: Authorization: Bearer {{appsmith.store.crmToken}}

// Query: get_agent_status (Redis)
// GET agent:status:{{agent_email}}

// These queries can be chained in JS

Writing Complex JS Queries

This is where Appsmith separates itself from simpler low-code tools. You write real JavaScript for business logic:

// JS Object: ticketActions
// This runs server-side in Appsmith's sandbox

export default {
  // Escalate a ticket: update status, reassign, notify manager
  async escalateTicket(ticketId, reason) {
    // Step 1: Get current ticket
    const ticket = await get_ticket.run({ id: ticketId })

    // Step 2: Find the escalation manager based on department
    const manager = await find_manager.run({
      department: ticket.department,
      level: "escalation",
    })

    // Step 3: Update the ticket
    await update_ticket.run({
      id: ticketId,
      status: "escalated",
      assigned_to: manager.email,
      escalation_reason: reason,
      escalated_at: new Date().toISOString(),
    })

    // Step 4: Log the escalation
    await create_audit_log.run({
      action: "ticket_escalated",
      ticket_id: ticketId,
      from_agent: ticket.assigned_to,
      to_agent: manager.email,
      reason: reason,
      timestamp: new Date().toISOString(),
    })

    // Step 5: Send notification via Novu
    await fetch("https://api.novu.co/v1/events/trigger", {
      method: "POST",
      headers: {
        "Authorization": `ApiKey ${appsmith.store.novuKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({
        name: "ticket-escalated",
        to: { subscriberId: manager.email, email: manager.email },
        payload: {
          ticketId,
          customerName: ticket.customer_name,
          reason,
          fromAgent: ticket.assigned_to,
        },
      }),
    })

    // Step 6: Refresh the ticket list
    await get_tickets.run()

    // Show success message
    showAlert("Ticket escalated to " + manager.email, "success")
  },

  // Bulk close resolved tickets older than 7 days
  async bulkCloseOldTickets() {
    const sevenDaysAgo = new Date()
    sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7)

    const tickets = await get_resolved_tickets.run({
      before_date: sevenDaysAgo.toISOString(),
    })

    let closed = 0
    for (const ticket of tickets) {
      await close_ticket.run({ id: ticket.id })
      closed++
    }

    showAlert(`Closed ${closed} tickets`, "success")
    await get_tickets.run()
  },
}

Problem

The bulk close function ran sequentially — each ticket took 200ms for the API call. With 500 tickets, it took 100 seconds. Appsmith's default timeout is 30 seconds. The function would fail halfway through.

What I Tried

Tried increasing the timeout in Appsmith settings. That helped but didn't scale. Tried batching the SQL updates into one query — better, but some tickets needed individual API calls.

Actual Fix

Use Promise.all for parallel execution and batch SQL updates:

// Parallel execution with controlled concurrency
async bulkCloseOldTickets() {
  const tickets = await get_resolved_tickets.run({})

  // Batch the SQL updates — one query instead of 500
  const ids = tickets.map(t => t.id)
  await bulk_close_tickets.run({ ids }) // UPDATE tickets SET status='closed' WHERE id = ANY({{ids}})

  // For individual API calls that can't be batched:
  // Use Promise.all with a concurrency limit
  const BATCH_SIZE = 10
  for (let i = 0; i < tickets.length; i += BATCH_SIZE) {
    const batch = tickets.slice(i, i + BATCH_SIZE)
    await Promise.all(
      batch.map(ticket => send_close_notification.run({ ticketId: ticket.id }))
    )
  }

  showAlert(`Closed ${tickets.length} tickets`, "success")
}

// In the PostgreSQL query: bulk_close_tickets
// UPDATE tickets
// SET status = 'closed', closed_at = NOW()
// WHERE id = ANY({{ids}}::int[])

Git Version Control for Apps

This is critical for team development. Appsmith's Git sync lets you branch, commit, and review changes like code:

# Connect Appsmith to Git
# 1. In Appsmith settings, go to "Git Sync"
# 2. Connect to your Git repository
# 3. Appsmith commits the app JSON to the repo

# The repo structure looks like:
# ├── ticket_system/
# │   ├── metadata.json        # App metadata
# │   ├── datasources/         # Database connections
# │   ├── pages/
# │   │   ├── Dashboard/
# │   │   │   ├── canvas.json  # UI layout
# │   │   │   └── queries/     # Page-specific queries
# │   │   └── TicketDetail/
# │   └── js-objects/          # Shared JS functions

# Workflow:
# 1. Developer creates a branch in Appsmith
# 2. Makes changes to the app
# 3. Commits and pushes to Git
# 4. PR review in GitHub/GitLab
# 5. Merge triggers deployment to production

# This means:
# - Multiple developers can work on the same app
# - Changes are reviewed before going live
# - You can roll back to any previous version
# - Audit trail of who changed what and when

Problem

Two developers edited the same page at the same time. Both pushed to Git. The JSON merge conflict left the app in a broken state — widgets had missing properties and some queries disappeared.

Actual Fix

Avoid merge conflicts by splitting work across pages or JS objects. If conflicts happen, resolve in Git and re-import:

# If merge conflict happens:
# 1. Don't try to fix it in Appsmith UI
# 2. Fix the JSON conflict in your Git tool
# 3. Accept one version of the conflicting file
# 4. Push the resolution
# 5. In Appsmith, pull the resolved version

# Prevention: assign pages to developers
# Developer A: Dashboard page + dashboard queries
# Developer B: TicketDetail page + ticket queries
# Developer C: JS objects + shared functions

Deploying to Production

# Self-hosted deployment with Docker
docker run -d \
  --name appsmith \
  -p 8080:80 \
  -v appsmith_stacks:/appsmith-stacks \
  appsmith/appsmith-ee:latest

# For production, use Docker Compose with:
# - PostgreSQL for Appsmith internal DB
# - MongoDB for Appsmith metadata
# - Redis for session management
# - Nginx for SSL termination

# docker-compose.yml highlights:
version: "3"
services:
  appsmith:
    image: appsmith/appsmith-ee:latest
    ports: ["8080:80"]
    volumes: ["./stacks:/appsmith-stacks"]
    environment:
      - APPSMITH_MAIL_ENABLED=true
      - APPSMITH_MAIL_HOST=smtp.example.com
      - APPSMITH_MAIL_USERNAME=notifications@example.com
      - APPSMITH_MAIL_PASSWORD=****
      - APPSMITH_DISABLE_TELEMETRY=true
      - APPSMITH_GOOGLE_MAPS_API_KEY=****

What I Learned

Wrapping Up

Appsmith replaced a spreadsheet-based support system for 50 agents. The ticket management app handles 200+ tickets/day, connects to three different data sources, and includes automated escalation logic. The JS query system handles complex business rules that simpler low-code tools can't. And the self-hosted deployment costs nothing beyond the server. For enterprise internal tools where cost and control matter, Appsmith is the right choice.

Related Articles